linux 内核调度
文章目录
一、Linux 内核常见的三种调度方法
-
SCHED_OTHER 分时调度策略
-
SCHED_FIFO 实时调度策略,先到先服务
-
SCHED_RR 实时调度策略,时间片轮转
实时进程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过 nice 和 counter 值决定权值,nice 越小,counter 越大,被调度的概率越大,也就是曾经使用了 cpu 最少的进程将会得到优先调度。
1.1 实时调度策略
SHCED_RR 和 SCHED_FIFO 的不同
当采用 SHCED_RR 策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的 RR 任务的调度公平。
SCHED_FIFO 一旦占用 cpu 则一直运行。一直运行直到有更高优先级任务到达或自己放弃。
如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO 时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而 RR 可以让每个任务都执行一段时间。
SHCED_RR 和 SCHED_FIFO 的相同点
- RR和FIFO都只用于实时任务。
- 创建时优先级大于 0 (1-99)。
- 按照可抢占优先级调度算法进行。
- 就绪态的实时任务立即抢占非实时任务。
如果所有任务都采用 FIFO,假设所有任务都采用 FIFO 调度策略时,则遵循如下原则:
- 创建进程时指定采用 FIFO,并设置实时优先级 rt_priority (1-99)。
- 如果没有等待资源,则将该任务加入到就绪队列中。
- 调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+ rt_priority ),选择权值最高的任务使用 cpu,该 FIFO 任务将一直占有 cpu 直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)。
- 调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒,等等),则调度程序立即在当前任务 堆栈中保存当前 cpu 寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到 cpu,此时高优先级的任务开始运行。重复第 3 步。
- 如果当前任务因等待资源而主动放弃 cpu 使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第 3 步。
如果所有任务都采用RR调度策略:
假设所有任务都采用RR调度策略时,则遵循如下原则:
- 创建任务时指定调度参数为 RR,并设置任务的实时优先级和 nice 值( nice 值将会转换为该任务的时间片的长度)。
- 如果没有等待资源,则将该任务加入到就绪队列中。
- 调度程序遍历就绪队列,根据实时优先级计算调度权值(1000 + rt_priority),选择权值最高的任务使用 cpu。
- 如果就绪队列中的 RR 任务时间片为 0,则会根据 nice 值设置该任务的时间片,同时将该任务放入就绪队列的末尾。重复步骤 3。
- 当前任务由于等待资源而主动退出 cpu,则其加入等待队列中。重复步骤 3。
1.2 分时调度策略
假设所有任务都采用 Linux 分时调度策略时,则遵循如下优先级:
-
创建任务指定采用分时调度策略,并指定优先级 nice 值(-20 ~ 19)。
-
将根据每个任务的 nice 值确定在 cpu 上的执行时间(counter)。
-
如果没有等待资源,则将该任务加入到就绪队列中。
-
调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算( counter + 20 - nice )结果,选择计算结果最大的一个去运行,当这 个时间片用完后( counter 减至 0 )或者主动放弃 cpu 时,该任务将被放在就绪队列末尾(时间片用完)或等待队列(因等待资源而放弃 cpu )中。
-
此时调度程序重复上面计算过程,转到第 4 步。
-
当调度程序发现所有就绪任务计算所得的权值都为不大于 0 时,重复第 2 步。
1.3 系统中既有分时调度又有时间片轮转调度和先进先出调度
这种状态也是最为常见的状态,这种情况下优先级遵循如下原则:
- RR 调度和 FIFO 调度的进程属于实时进程,以分时调度的进程是非实时进程。
- 当实时进程准备就绪后,如果当前 cpu 正在运行非实时进程,则实时进程立即抢占非实时进程。
- RR 进程和 FIFO 进程都采用实时优先级做为调度的权值标准,RR 是 FIFO 的一个延伸。FIFO 时,如果两个进程的优先级一样,则这两个优先级一样的进程具体执行哪一个是由其在队列中的位置决定的,这样导致一些不公正性(优先级是一样的,为什么要让你一直运行?),如果将两个优先级一样的任务的调度策略都设为 RR,则保证了这两个任务可以循环执行,保证了公平。
二、调度器的基础知识
2.1 调度的概述
多任务操作系统分为非抢占式多任务和抢占式多任务。与大多数现代操作系统一样,Linux 采用的是抢占式多任务模式。这表示对 CPU 的占用时间由操作系统决定的,具体为操作系统中的调度器。调度器决定了什么时候停止一个进程以便让其他进程有机会运行,同时挑选出一个其他的进程开始运行。在 Linux 上调度策略决定了调度器是如何选择一个新进程的时间。调度策略与进程的类型有关,内核现有的调度策略如下:
bionic/tools/versioner/dependencies/common/kernel_uapi/linux/sched.h
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
0:默认的调度策略,针对的是普通进程。
1:针对实时进程的先进先出调度。适合对时间性要求比较高但每次运行时间比较短的进程。
2:针对的是实时进程的时间片轮转调度。适合每次运行时间比较长得进程。
3:针对批处理进程的调度,适合那些非交互性且对 cpu 使用密集的进程。
5:适用于优先级较低的后台进程。
6:SCHED_DEADLINE 实现了 Earliest Deadline First (EDF) 调度算法,目前暂未完善。
注:每个进程的调度策略保存在进程描述符 task_struct 中的 policy 字段。
2.2 调度器中的机制
内核引入调度类(struct sched_class)说明了调度器应该具有哪些功能。内核中每种调度策略都有该调度类的一个实例。(比如:基于公平调度类为:fair_sched_class,基于实时进程的调度类实例为:rt_sched_class),该实例也是针对每种调度策略的具体实现。调度类封装了不同调度策略的具体实现,屏蔽了各种调度策略的细节实现。
调度器核心函数 schedule() 只需要调用调度类中的接口,完成进程的调度,完全不需要考虑调度策略的具体实现。调度类连接了调度函数和具体的调度策略。
调度类就是代表的各种调度策略,调度实体就是调度单位,这个实体通常是一个进程,但是自从引入了 cgroup 后,这个调度实体可能就不是一个进程了,而是一个组。
2.3 schedule() 函数
在第一部分已经讲过,Linux 支持两种类型的进程调度,三种常见的调度策略。即实时进程采用 SCHED_FIFO 和 SCHED_RR 调度策略,普通进程采用 SCHED_NORMAL 策略。
/*
* __schedule() is the main scheduler function.
*
* The main means of driving the scheduler and thus entering this function are:
*
* 1. Explicit blocking: mutex, semaphore, waitqueue, etc.
*
* 2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
* paths. For example, see arch/x86/entry_64.S.
*
* To drive preemption between tasks, the scheduler sets the flag in timer
* interrupt handler scheduler_tick().
*
* 3. Wakeups don't really cause entry into schedule(). They add a
* task to the run-queue and that's it.
*
* Now, if the new task added to the run-queue preempts the current
* task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
* called on the nearest possible occasion:
*
* - If the kernel is preemptible (CONFIG_PREEMPT=y):
*
* - in syscall or exception context, at the next outmost
* preempt_enable(). (this might be as soon as the wake_up()'s
* spin_unlock()!)
*
* - in IRQ context, return from interrupt-handler to
* preemptible context
*
* - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
* then at the next:
*
* - cond_resched() call
* - explicit schedule() call
* - return from syscall or exception to user-space
* - return from interrupt-handler to user-space
*
* WARNING: must be called with preemption disabled!
*/
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;
u64 wallclock;
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
schedule_debug(prev);
if (sched_feat(HRTICK