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))
hrtick_clear(rq);
local_irq_disable();
rcu_note_context_switch(preempt);
/*
* 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().
*/
rq_lock(rq, &rf);
smp_mb__after_spinlock();
/* Promote REQ to ACT */
rq->clock_update_flags <<= 1;
update_rq_clock(rq);
switch_count = &prev->nivcsw;
if (!preempt && prev->state) {
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
prev->on_rq = 0;
if (prev->in_iowait) {
atomic_inc(&rq->nr_iowait);
delayacct_blkio_start();
}
/*
* 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, &rf);
}
}
switch_count = &prev->nvcsw;
}
next = pick_next_task(rq, prev, &rf);
wallclock = walt_ktime_clock();
walt_update_task_ravg(prev, rq, PUT_PREV_TASK, wallclock, 0);
walt_update_task_ravg(next, rq, PICK_NEXT_TASK, wallclock, 0);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
if (likely(prev != next)) {
#ifdef CONFIG_SCHED_WALT
if (!prev->on_rq)
prev->last_sleep_ts = wallclock;
#endif
rq->nr_switches++;
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. For TSO
* (e.g. x86), the architecture must provide its own
* barrier in switch_mm(). For weakly ordered machines
* for which spin_unlock() acts as a full memory
* barrier, finish_lock_switch() in common code takes
* care of this barrier. For weakly ordered machines for
* which spin_unlock() acts as a RELEASE barrier (only
* arm64 and PowerPC), arm64 has a full barrier in
* switch_to(), and PowerPC has
* smp_mb__after_unlock_lock() before
* finish_lock_switch().
*/
++*switch_count;
trace_sched_switch(preempt, prev, next);
/* 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);
}
preempt_disable():禁止内核抢占
cpu_rq():获取当前 cpu 对应的就绪队列。
prev = rq->curr:获取当前进程的描述符 prev
switch_count = &prev->nivcsw:获取当前进程的切换次数。
update_rq_clock():更新就绪队列上的时钟
clear_tsk_need_resched():清除当前进程 prev 的重新调度标志。
deactive_task():将当前进程从就绪队列中删除。
put_prev_task():将当前进程重新放入就绪队列
pick_next_task():在就绪队列中挑选下一个将被执行的进程。
context_switch():进行 prev 和 next 两个进程的切换。具体的切换代码与体系架构有关,在 switch_to() 中通过一段汇编代码实现。
post_schedule():进行进程切换后的后期处理工作。
2.4 pick_next_task 函数
pick_next_task 是进程调度的关键步骤,主要功能是从发生调度的 CPU 的运行队列中选择一个进程运行。系统中的调度顺序是:实时进程–>普通进程–>空闲进程。分别从属于三个调度类:rt_sched_class,fair_sched_class 和 idle_sched_class,代码实现如下:
/*
* Pick up the highest-prio 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->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->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->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->pick_next_task(rq, prev, rf);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
/* The idle class should always have a runnable task: */
BUG();
}
pick_next_task() 的执行过程可以分为以下两个步骤:
步骤 1:检查运行队列中是否含有实时进程,if 语句中判断了当前 cpu 就绪队列中的进程数目是否与普通进程的就绪队列中的进程数目相同,如果相同就说明了系统中全是普通进程,直接通过 cfs 算法(完全公平调度算法 Completely Fair Scheduler)的调度类的 pick_next_task_fair 函数来从普通进程的就绪队列中寻找进程即可。
cfs 算法(完全公平调度算法)的调度类的 pick_next_task_fair 函数返回值有三种情况:
- RETRY_TASK:表示有从属于更高优先级调度类的进程被唤醒,跳转到步骤 2。
- 空指针:表示没有 cfs 调度类的进程处于就绪态,在idle调度类中寻找进程。
- struct task 结构体指针:表示选择到一个进程,返回指针。
步骤 2:遍历调度类的链表,并从中选择一个优先级最高的进程。
在内核中的所有现有调度类是按优先级排列在调度类链表中。
当 rq 中的运行队列的个数 (nr_running) 和 cfs 中的 nr_runing 相等的时候,表示现在所有的都是普通进程,这时候就会调用 cfs 算法中的 pick_next_task,当不相等的时候,则调用 sched_class_highest,这下面的这个 for(;😉 循环中,首先是会在实时进程中选取要调度的程序(p = class->pick_next_task(rq);)。如果没有选取到,会执行 class=class->next;在 class 这个链表中有三种类型(fair,idle,rt),也就是说会调用到下一个调度类。
2.5 调度中的 nice 值
首先要明确的是: nice 的值不是进程的优先级,他们不是一个概念,但是进程的 nice 值会影响到进程的优先级变化。
通过命令 ps -lA 可以看到进程的 nice 值为 NI 列。PRI 表示的是进程的优先级,其实进程的优先级只是一个整数,它是调度器选择进程运行的基础。
1|/ $ ps -lA
F S UID PID PPID C PRI NI BIT SZ WCHAN TTY TIME CMD
4 S 0 1 0 0 19 0 - 2712193 0 ? 00:00:05 init
1 S 0 2 0 0 19 0 - 0 0 ? 00:00:00 kthreadd
1 I 0 4 2 0 39 -20 - 0 0 ? 00:00:00 kworker/0:0H
1 I 0 6 2 0 39 -20 - 0 0 ? 00:00:00 mm_percpu_wq
1 S 0 7 2 0 19 0 - 0 0 ? 00:00:00 ksoftirqd/0
1 I 0 8 2 0 19 0 - 0 0 ? 00:00:06 rcu_preempt
1 I 0 9 2 0 19 0 - 0 0 ? 00:00:00 rcu_sched
1 I 0 10 2 0 19 0 - 0 0 ? 00:00:00 rcu_bh
1 S 0 11 2 0 139 0 - 0 0 ? 00:00:00 migration/0
1 S 0 12 2 0 19 0 - 0 0 ? 00:00:00 cpuhp/0
1 S 0 13 2 0 19 0 - 0 0 ? 00:00:00 cpuhp/1
1 S 0 14 2 0 139 0 - 0 0 ? 00:00:00 migration/1
1 S 0 15 2 0 19 0 - 0 0 ? 00:00:00 ksoftirqd/1
1 I 0 17 2 0 39 -20 - 0 0 ? 00:00:00 kworker/1:0H
普通进程有:静态优先级和动态优先级。
静态优先级:之所有称为静态优先级是因为它不会随着时间而改变,内核不会修改它,只能通过系统调用 nice 去修改,静态优先级用进程描述符中的 static_prio 来表示。在内核中 /kernel/sched 中,nice 和静态优先级的关系为:
/*
* Convert user-nice values [ -20 ... 0 ... 19 ]
* to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],
* and back.
*/
#define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO)
#define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO)
动态优先级:调度程序通过增加或者减小进程静态优先级的值来奖励 IO 小的进程或者惩罚 cpu 消耗型的进程。调整后的优先级称为动态优先级。在进程描述中用 prio 来表示,通常所说的优先级指的是动态优先级。
可以通过系统调用 nice 函数来改变进程的优先级。
2.6 获取和设置调度策略
Android 系统代码中可以通过 Process.getThreadScheduler 和 Process.setThreadScheduler。
/**
* Return the current scheduling policy of a thread, based on Linux.
*
* @param tid The identifier of the thread/process to get the scheduling policy.
*
* @throws IllegalArgumentException Throws IllegalArgumentException if
* <var>tid</var> does not exist, or if <var>priority</var> is out of range for the policy.
* @throws SecurityException Throws SecurityException if your process does
* not have permission to modify the given thread, or to use the given
* scheduling policy or priority.
*
* {@hide}
*/
@TestApi
public static final native int getThreadScheduler(int tid)
throws IllegalArgumentException;
/**
* Set the scheduling policy and priority of a thread, based on Linux.
*
* @param tid The identifier of the thread/process to change.
* @param policy A Linux scheduling policy such as SCHED_OTHER etc.
* @param priority A Linux priority level in a range appropriate for the given policy.
*
* @throws IllegalArgumentException Throws IllegalArgumentException if
* <var>tid</var> does not exist, or if <var>priority</var> is out of range for the policy.
* @throws SecurityException Throws SecurityException if your process does
* not have permission to modify the given thread, or to use the given
* scheduling policy or priority.
*
* {@hide}
*/
public static final native void setThreadScheduler(int tid, int policy, int priority)
throws IllegalArgumentException;
调试中也可以使用 adb 命令来更改调度策略:
使用 chrt 命令,可以更改进程的调度策略和优先级。
使用 chrt --help 查看 chrt 指令:
:/ $ chrt --help
usage: chrt [-Rmofrbi] {-p PID [PRIORITY] | [PRIORITY COMMAND...]}
Get/set a process' real-time scheduling policy and priority.
-p Set/query given pid (instead of running COMMAND)
-R Set SCHED_RESET_ON_FORK
-m Show min/max priorities available
Set policy (default -r):
-o SCHED_OTHER -f SCHED_FIFO -r SCHED_RR
-b SCHED_BATCH -i SCHED_IDLE
设置优先级的数值时可以参考如下内容,在内核 task_struct 中有四个字段表示优先级,分别是:static_prio,rt_priority,normal_prio,prio,这四种优先级需要注意 rt_priority 不是越小优先级越高,而是越大优先级越高:
静态优先级 实时优先级 普通优先级 动态优先级
static_proc rt_priority normal_prio prio
rt_priority 值为 0 ~ 99,0 是普通进程,1 ~ 99 是实时进程,99 优先级最高
normal_prio 值为 -1 ~ 139
prio 动态优先级
(1)值越小,进程优先级越高
(2)prio 值为 -1 ~139
(3)调度器调度采用该优先级调度。该优先级会根据是否为实时进程采用下面的三个优先级来计算
static_prio 静态优先级
(1)值越小,进程优先级越高
(2)static_prio 值为 100~139,static_prio = nice + MAX_RT_PRIO + NICE_WIDTH / 2
(3)缺省值是 120
(4)用户空间可以通过 nice() 或者 setpriority 对该值进行修改。通过 getpriority 可以获取该值。
(5)新创建的进程会继承父进程的 static priority。
normal_prio 动态优先级,
(1)值越小,进程优先级越高
(2)normal_prio 值为 -1 ~139
(3)表示基于进程的静态优先级 static_prio 和调度策略计算出的优先级。普通进程 static_prio = normal_prio
(4)进程 fork 时, 子进程会继承父进程的普通优先级
rt_priority 实时优先级
(1)用于表示实时进程优先级
(2)值越大,进程优先级越高
(3)rt_priority 值为 0~99,0 是普通进程,1~99 是实时进程,99 的优先级最高
ystrace 中看到的优先级是越小越高,在 ps -lA 看到的优先级是越大优先级越高,ps 看到 0 - 39 对应内核优先级中的 100 - 139,而 40 - 139 对应内核优先级中的 RT 优先级 0 - 99,实时优先级是越大优先级越高,在内核设置实时调度策略 0~99 越大优先级越高。