调度策略
对于操作系统来说,cpu的数量是有限的,有些任务优先级较高,有些任务优先级较低,为了有效的利用cpu,这时就有了进程的调度的概念。
在task_struct中,有一个成员变量,用来表示调度策略。
unsigned
int policy;
在sched.h文件头 有这样一个#include <uapi/linux/sched.h>
打开这个文件,会看到如下定义,是
policy
的几个定义。
/*
*
Scheduling
policies
*/
#
define
SCHED_NORMAL 0
#
define
SCHED_FIFO 1
#
define
SCHED_RR 2
#
define
SCHED_BATCH 3
/* SCHED_ISO:
reserved
but
not
implemented
yet
*/
#
define
SCHED_IDLE 5
#
define
SCHED_DEADLINE 6
有了调度策略的定义,那么每个策略的优先级是如何划分的?
为了配合调度策略,当然也有优先级的字段,在task_struct中,
int prio;
int static_prio;
int normal_prio;
unsigned
int rt_priority;
优先级是通过数字来表示的,可以在#include <linux/sched/prio.h>
文件里找到定义。
有这样一块注释
/*
*
Priority
of a
process
goes
from
0..MAX_PRIO-1,
valid
RT
*
priority
is 0..MAX_RT_PRIO-1,
and
SCHED_NORMAL/SCHED_BATCH
*
tasks
are
in
the
range
MAX_RT_PRIO..MAX_PRIO-1
*/
#
define
MAX_USER_RT_PRIO 100
#
define
MAX_RT_PRIO MAX_USER_RT_PRIO
#
define
MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)
/*
*RT
priority
to be
separate
from
the
value
exported
to
*
user
-
space
.
This
allows
kernel
threads
to
set
their
*
priority
to a
value
higher
than
any
user
task
.
*/
-
通过代码可以得到 SCHED_NORMAL/SCHED_BATCH等普通进程的优先级数值是100-139。
-
通过注释可以看到实时进程的优先级要高于普通进程的,其数值是0-99
进程的分类
上面我们知道了进程优先级实时进程高于普通进程,那么具体的分类是什么
-
实时进程
- SCHED_FIFO 先进先出算法。
- SCHED_RR 轮流调度算法。
- SCHED_DEADLINE 按照deadline的时间来选择,每次选择最近的deadline时间的任务来执行。
-
普通进程
- SCHED_NORMAL 普通进程
- SCHED_BATCH 后台进程,与我们平时了解的后台任务一样。
- SCHED_IDEL优先级最低的任务,在空闲期执行。
调度的执行逻辑
有了调度的策略,此时就需要执行调度任务,在 task_struct 中有这样一个字段
const
struct
sched_class *sched_class;
这个结构体定义在kernel/sched/sched.h
文件中,其中有如下实现
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;
- stop_sched_class: 优先级最高,会中断所有其他进程,且不会被其他任务打断
- dl_sched_class: 对应deadline调度策略
- rt_sched_class:对应RR或者FIFO调度策略,具体由policy决定
- fair_sched_class: 普通进程调度策略
- idle_sched_class: 空闲进程调度策略
普通进程
完全公平调度算法
CFS调度算法:Completely Fair Scheduling
CSF中依靠vruntime实现进程优先级,vruntime小的先进行调度,csf需要每次对vruntime进行排序并且进行平衡,那么所使用的数据结构是红黑树。
具体的实现逻辑在kernel/sched/fair.c
文件中
static
void
update_curr(
struct
cfs_rq *cfs_rq)
{
struct
sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (
unlikely
(!curr))
return
;
delta_exec = now - curr->exec_start;
if (
unlikely
((s64)delta_exec <= 0))
return
;
curr->exec_start = now;
schedstat_set(curr->statistics.exec_max,
max(delta_exec, curr->statistics.exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq->exec_clock, delta_exec);
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
if (entity_is_task(curr)) {
struct
task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cgroup_account_cputime(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
下面是红黑树的结构,节点中包括vruntime和load_weight
struct
sched_entity {
/*
For
load
-
balancing
: */
struct
load_weight load;
unsigned
long
runnable_weight;
struct
rb_node run_node;
struct
list_head group_node;
unsigned
int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 nr_migrations;
struct
sched_statistics statistics;
执行流程
CPU -> 队列rq -> rt_rq和cfs_rq
CPU需要找下一个任务执行的时候,rt_sched_class先调用,它会在rt_rq队列上找,如果rt_rq找不到,才轮到fair_sched_class被调用,它会在cfs_rq队列上查找,这样保证了实时进程的优先级永远大于普通进程。
进程调度
1.主动调度
- Btrfs,等待写入
- 从Tap网络设备等待一个读取
使用schedule 函数进行调度,实现代码位于kernel/sched/core.c
的static void __sched notrace __schedule(bool preempt**)
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;
...
next = pick_next_task(rq, prev, &rf);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
...
}
...
if (
likely
(prev != next)) {
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
.
*
*
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);
/*
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);
}
pick_next_task
方法是获取下一个调度任务
/*
*
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();
}
优先判断的是普通进程,如果普通进程则调用fair_sched_class,位于fair.c文件中,并且实现了 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 = &rq->cfs;
struct
sched_entity *se;
struct
task_struct *p;
int new_tasks;
again:
if (!cfs_rq->nr_running)
goto
idle;
...
struct
sched_entity *curr = cfs_rq->curr;
if (curr) {
if (curr->on_rq)
update_curr(cfs_rq);
else
curr =
NULL
;
...
se = pick_next_entity(cfs_rq, curr);
cfs_rq = group_cfs_rq(se);
}
while
(cfs_rq);
p = task_of(se);
if (prev != p) {
struct
sched_entity *pse = &prev->se;
...
put_prev_entity(cfs_rq, pse);
set_next_entity(cfs_rq, se);
可以看到从cfs_rq队列中取出当前运行的任务,如果当前任务是可运行的,则调用update_curr
更新vruntime。接着 pick_next_entity 从红黑树中取出最小的vruntime的节点。
task_of获得一个task_struct实例,继任和前任如果不相同,就更新红黑树,将前任的vruntime 更新后放入put_prev_entity
,set_next_entity
将继任者设置为当前任务。
进程上下文切换
从__schedule()中我们可以看到当继任和前任不同时,会调用rq = context_switch(rq, prev, next, &rf);
即进行上下文切换,包括下列2个功能
-
切换进程空间
-
切换寄存器和CPU上下文
-
x86结构体系中,提供了结构体 TSS ( Task State Segment ),用以存放所有的寄存器。
-
特殊的寄存器( Task Register )任务寄存器,指向某个进程的TSS,更改TR的值,将会触发硬件将CPU所有寄存器的值保存到TSS中,然后从新的进程中读取出 TSS 保存的寄存器的值,并加载到CPU对应的寄存器中。
-
系统层次的优化:cpu_init 时 给每一个cpu关联一个TSS,然后TR一直指向这个TSS,在task_struct 中结构体 thread_struct 用来存放切换进程需要修改的寄存器
/* CPU - specific state of this task : */ struct thread_struct thread;
所以进程切换就是将 thread_struct 存放的寄存器的值写入到CPU的TR指向的TSS。
-
2.抢占式调度
-
一个进程执行时间过长,是时候切换到另一个进程
- 时钟中断处理函数
scheduler_tick
位于kernel/sched/core.c
中 - 处理时钟事件
curr->sched_class->task_tick(rq, curr,0 );
,通过调用可以发现每一个sched_class 实现的task_tick不同。 - 当发现一个进程被抢占时,不会将它踢下来,而是标记成被抢占。要一直等待该进程运行到__schedule()。
- 时钟中断处理函数
-
当一个进程被唤醒的时候
-
检查并标记抢占进程
try_to_wake_up()->ttwu_queue()
将唤醒任务加入队列 调用ttwu_do_activate
激活任务- 调用
tt_do_wakeup()->check_preempt_curr()
检查是否应该抢占, 若需抢占则标记
-
用户态抢占时机
- 从系统调用中返回, 返回过程中会调用
exit_to_usermode_loop
, 若打了标记, 则调用 schedule() - 从中断中返回, 中断返回分为返回用户态和内核态(汇编代码: arch/x86/entry/entry_64.S),
- 从系统调用中返回, 返回过程中会调用
-
内核态抢占时机
- 一般发生在
preempt_enable()
中, 内核态进程有的操作不能被中断, 想要进行抢占之前,会调用preempt_disable()
关闭抢占, 在开启(调用preempt_enable()
) 是一个抢占时机, 会调用preempt_count_dec_and_test(),
检测 preempt_count 和标记, 若可抢占则最终调用preempt_schedule->preempt_schedule_common->__schedule
- 发生在中断返回, 也会调用
preempt_schedule_irq->__schedule
- 一般发生在
-