- 1 前景回顾
- 1.1 Linux的调度器组成
- 1.1.1 2个调度器
- 1.1.2 6种调度策略
- 1.1.3 5个调度器类
- 1.1.4 3个调度实体
- 1.1.5 调度器整体框架
- 1.1 Linux的调度器组成
- 2 linux优先级的表示
- 2.1 优先级的内核表示
- 2.1.1 linux优先级概述
- 2.1.2 内核的优先级表示
- 2.1.3 DEF最早截至时间优先实时调度算法的优先级描述
- 2.2 进程的优先级表示
- 2.2.1 动态优先级 静态优先级 实时优先级
- 2.1 优先级的内核表示
- 3 进程优先级的计算
- 3.1 normal_prio()设置普通优先级normal_prio
- 3.1.1 辅助函数task_has_dl_policy和task_has_rt_policy
- 3.1.2 关于rt_priority数值越大, 实时进程优先级越高的问题
- 3.1.3 为什么需要__normal_prio函数
- 3.2 effective_prio设置动态优先级prio
- 3.2.1 为什么effective_prio使用优先级数值检测实时进程
- 3.3 设置prio的时机
- 3.4 nice系统调用的实现
- 3.5 fork时优先级的继承
- 3.1 normal_prio()设置普通优先级normal_prio
- 4 总结
- 5 参考
1 前景回顾
1.1 Linux的调度器组成
1.1.1 2个调度器
可以用两种方法来激活调度
-
一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
-
另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要
因此当前linux的调度程序由两个调度器组成:
-
主调度器
-
周期性调度器
两者又统称为通用调度器(generic scheduler)或核心调度器(core scheduler)
并且每个调度器包括两个内容:调度框架(其实质就是两个函数框架)及调度器类
1.1.2 6种调度策略
linux内核目前实现了6种调度策略(即调度算法), 用于对不同类型的进程进行调度,或者支持某些特殊的功能
-
SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程
-
SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程
-
SCHED_IDLE则在系统空闲时调用idle进程.
1.1.3 5个调度器类
而依据其调度策略的不同实现了5个调度器类,一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.
其所属进程的优先级顺序为
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
1.1.4 3个调度实体
调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度.
这种一般性要求调度器不直接操作进程,而是处理可调度实体,因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构,其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组.
linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体
-
sched_dl_entity 采用EDF算法调度的实时调度实体
-
sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体
-
sched_entity 采用CFS算法调度的普通非实时进程的调度实体
1.1.5 调度器整体框架
每个进程都属于某个调度器类(由字段task_struct->sched_class标识),由调度器类采用进程对应的调度策略调度(由task_struct->policy)进行调度, task_struct也存储了其对应的调度实体标识
linux实现了6种调度策略,依据其调度策略的不同实现了5个调度器类,一个调度器类可以用一种或者多种调度策略调度某一类进程,也可以用于特殊情况或者调度特殊功能的进程.
调度器类 | 调度策略 | 调度策略对应的调度算法 | 调度实体 | 调度实体对应的调度对象 |
---|---|---|---|---|
stop_sched_class | 无 | 无 | 无 | 特殊情况,发生在cpu_stop_cpu_callback进行cpu之间任务迁移migration或者HOTPLUG_CPU的情况下关闭任务 |
dl_sched_class | SCHED_DEADLINE | Earliest-Deadline-First最早截至时间有限算法 | sched_dl_entity | 采用DEF最早截至时间有限算法调度实时进程 |
rt_sched_class | SCHED_RR SCHED_FIFO |
Roound-Robin时间片轮转算法 FIFO先进先出算法 |
sched_rt_entity | 采用Roound-Robin或者FIFO算法调度的实时调度实体 |
fair_sched_class | SCHED_NORMAL SCHED_BATCH |
CFS完全公平懂调度算法 | sched_entity | 采用CFS算法普通非实时进程 |
idle_sched_class | SCHED_IDLE | 无 | 无 | 特殊进程, 用于cpu空闲时调度空闲进程idle |
调度器组成的关系如下图
2 linux优先级的表示
2.1 优先级的内核表示
2.1.1 linux优先级概述
在用户空间通过nice命令设置进程的静态优先级,这在内部会调用nice系统调用,进程的nice值在-20~+19之间(用户空间!!!。).值越低优先级越高.
setpriority系统调用也可以用来设置进程的优先级.它不仅能够修改单个线程的优先级, 还能修改进程组中所有进程的优先级,或者通过制定UID来修改特定用户的所有进程的优先级(特定用户!!!)
内核使用一些简单的数值范围0~139表示内部优先级(内核里面使用!!!), 数值越低, 优先级越高。
-
0~99的范围专供实时进程使用
-
nice的值[-20,19]则映射到范围100~139, 用于普通进程
linux2.6内核将任务优先级进行了一个划分, 实时进程优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139).
内核里面priority的范围:
优先级范围 | 内核宏 | 描述 |
---|---|---|
0 —— 99 | 0 —— (MAX_RT_PRIO - 1) | 实时进程 |
100 —— 139 | MAX_RT_PRIO —— (MAX_PRIO - 1) | 非实时进程 |
2.1.2 内核的优先级表示
内核表示优先级的所有信息基本都放在include/linux/sched/prio.h中, 其中定义了一些表示优先级的宏和函数.
优先级数值通过宏来定义, 如下所示,
其中MAX_NICE和MIN_NICE定义了nice的最大最小值
而MAX_RT_PRIO指定了实时进程的最大优先级,而MAX_PRIO则是普通进程的最大优先级数值
/* http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L4 */
#define MAX_NICE 19
#define MIN_NICE -20
#define NICE_WIDTH (MAX_NICE - MIN_NICE + 1)
/* http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L24 */
#define MAX_PRIO (MAX_RT_PRIO + 40)
#define DEFAULT_PRIO (MAX_RT_PRIO + 20)
宏 | 值 | 描述 |
---|---|---|
MIN_NICE | -20 | 对应于优先级100,可以使用NICE_TO_PRIO和PRIO_TO_NICE转换 |
MAX_NICE | 19 | 对应于优先级139,可以使用NICE_TO_PRIO和PRIO_TO_NICE转换 |
NICE_WIDTH | 40 | nice值得范围宽度, 即[-20, 19]共40个数字的宽度 |
MAX_RT_PRIO, MAX_USER_RT_PRIO | 100 | 实时进程的最大优先级 |
MAX_PRIO | 140 | 普通进程的最大优先级 |
DEFAULT_PRIO | 120 | 进程的默认优先级, 对应于nice=0 |
MAX_DL_PRIO | 0 | 使用EDF最早截止时间优先调度算法的实时进程最大的优先级 |
而内核提供了一组宏将优先级在各种不同的表示形之间转移
// http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L27
/*
* 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)
/*
* 'User priority' is the nice value converted to something we
* can work with better when scaling various scheduler parameters,
* it's a [ 0 ... 39 ] range.
*/
#define USER_PRIO(p) ((p)-MAX_RT_PRIO)
#define TASK_USER_PRIO(p) USER_PRIO((p)->static_prio)
#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))
还有一些nice值和rlimit值之间相互转换的函数nice_to_rlimit和rlimit_to_nice, 这在nice系统调用进行检查的时候很有用,他们定义在include/linux/sched/prio.h, L47中, 如下所示
/*
* Convert nice value [19,-20] to rlimit style value [1,40].
*/
static inline long nice_to_rlimit(long nice)
{
return (MAX_NICE - nice + 1);
}
/*
* Convert rlimit style value [1,40] to nice value [-20, 19].
*/
static inline long rlimit_to_nice(long prio)
{
return (MAX_NICE - prio + 1);
}
2.1.3 DEF最早截至时间优先实时调度算法的优先级描述
此外新版本的内核还引入了EDF实时调度算法,它的优先级比RT进程和NORMAL/BATCH进程的优先级都要高,关于EDF的优先级的设置信息都在内核头文件include/linux/sched/deadline.h
因此内核将MAX_DL_PRIO设置为0,可以参见内核文件include/linux/sched/deadline.h
#define MAX_DL_PRIO 0
此外也提供了一些EDF优先级处理所需的函数,如下所示,可以参见内核文件include/linux/sched/deadline.h
static inline int dl_prio(int prio)
{
if (unlikely(prio < MAX_DL_PRIO))
return 1;
return 0;
}
static inline int dl_task(struct task_struct *p)
{
return dl_prio(p->prio);
}
static inline bool dl_time_before(u64 a, u64 b)
{
return (s64)(a - b) < 0;
}
2.2 进程的优先级表示
struct task_struct
{
/* 进程优先级
* prio: 动态优先级,范围为100~139,与静态优先级和补偿(bonus)有关
* static_prio: 静态优先级,static_prio = 100 + nice + 20 (nice值为-20~19,所以static_prio值为100~139)
* normal_prio: 没有受优先级继承影响的常规优先级,具体见normal_prio函数,跟属于什么类型的进程有关
*/
int prio, static_prio, normal_prio;
/* 实时进程优先级 */
unsigned int rt_priority;
}
2.2.1 动态优先级 静态优先级 实时优先级
其中task_struct采用了三个成员表示进程的优先级:
- prio和normal_prio表示动态优先级
- static_prio表示进程的静态优先级.
为什么表示动态优先级需要两个值prio和normal_prio
调度器会考虑的优先级则保存在prio.由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用prio表示.由于这些改变不是持久的,因此静态优先级static_prio和普通优先级normal_prio不受影响.
此外还用了一个字段rt_priority保存了实时进程的优先级
字段 | 描述 |
---|---|
static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定 |
prio | 保存进程的动态优先级 |
normal_prio | 表示基于进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级 |
rt_priority | 用于保存实时优先级 |
实时进程的优先级用实时优先级rt_priority来表示
3 进程优先级的计算
前面说了task_struct中的几个优先级的字段
静态优先级 | 普通优先级 | 动态优先级 | 实时优先级 |
---|---|---|---|
static_prio | normal_prio | prio | rt_priority |
但是这些优先级是如何关联的呢, 动态优先级prio又是如何计算的呢?
3.1 normal_prio()设置普通优先级normal_prio
静态优先级static_prio(普通进程)和实时优先级rt_priority(实时进程)是计算的起点(!!!)
因此他们也是进程创建的时候设定好的,我们通过nice修改的就是普通进程的静态优先级static_prio(!!!。)
首先通过静态优先级static_prio计算出普通优先级normal_prio, 该工作可以由normal_prio来完成,该函数定义在kernel/sched/core.c#L861
/*
* __normal_prio - return the priority that is based on the static prio
* 普通进程(非实时进程)的普通优先级normal_prio就是静态优先级static_prio
*/
static inline int __normal_prio(struct task_struct *p)
{
return p->static_prio;
}
/*
* Calculate the expected normal priority: i.e. priority
* without taking RT-inheritance into account. Might be
* boosted by interactivity modifiers. Changes upon fork,
* setprio syscalls, and whenever the interactivity
* estimator recalculates.
*/
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_dl_policy(p)) /* EDF调度的实时进程 */
prio = MAX_DL_PRIO-1;
else if (task_has_rt_policy(p)) /* 普通实时进程的优先级 */
prio = MAX_RT_PRIO-1 - p->rt_priority;
else /* 普通进程的优先级 */
prio = __normal_prio(p);
return prio;
}
进程类型 | 调度器 | 普通优先级normal_prio |
---|---|---|
EDF实时进程 | EDF | MAX_DL_PRIO - 1 = -1 |
实时进程 | RT | MAX_RT_PRIO - 1 - p->rt_priority = 99 - rt_priority |
普通进程 | CFS | __normal_prio(p) = static_prio |
普通优先级normal_prio需要根据普通进程和实时进程进行不同的计算, 其中__normal_prio适用于普通进程,直接将普通优先级normal_prio设置为静态优先级static_prio.而实时进程的普通优先级计算依据其实时优先级rt_priority.
3.1.1 辅助函数task_has_dl_policy和task_has_rt_policy
定义在kernel/sched/sched.h 中
其本质其实就是传入task->policy调度策略字段看其值等于SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE中的哪个, 从而确定其所属的调度类, 进一步就确定了其进程类型
static inline int idle_policy(int policy)
{
return policy == SCHED_IDLE;
}
static inline int fair_policy(int policy)
{
return policy == SCHED_NORMAL || policy == SCHED_BATCH;
}
static inline int rt_policy(int policy)
{
return policy == SCHED_FIFO || policy == SCHED_RR;
}
static inline int dl_policy(int policy)
{
return policy == SCHED_DEADLINE;
}
static inline bool valid_policy(int policy)
{
return idle_policy(policy) || fair_policy(policy) ||
rt_policy(policy) || dl_policy(policy);
}
static inline int task_has_rt_policy(struct task_struct *p)
{
return rt_policy(p->policy);
}
static inline int task_has_dl_policy(struct task_struct *p)
{
return dl_policy(p->policy);
}
3.1.2 关于rt_priority数值越大, 实时进程优先级越高的问题
我们前面提到了数值越小,优先级越高, 但是此处我们会发现rt_priority的值越大,其普通优先级越小,从而优先级越高.
因此网上出现了一种说法, 优先级越高?这又是怎么回事?难道有一种说法错了吗?
实际的原因是这样的,对于一个实时进程(!!!),他有两个参数来表明优先级(!!!)——prio 和 rt_priority,
prio才是调度所用的最终优先级数值(!!!),这个值越小,优先级越高;
而rt_priority被称作实时进程优先级,prio要经过转化——prio=MAX_RT_PRIO - 1 - p->rt_priority;
MAX_RT_PRIO = 100;这样意味着rt_priority值越大,优先级越高;
而内核提供的修改优先级的函数,是修改rt_priority的值,所以越大,优先级越高。
所以用户在使用实时进程或线程,在修改优先级时,就会有“优先级值越大,优先级越高的说法”,也是对的。
3.1.3 为什么需要__normal_prio函数
我们肯定会奇怪, 为什么增加了一个__normal_prio函数做了这么简单的工作,这个其实是有历史原因的:在早期的$O(1)$调度器中,普通优先级的计算涉及相当多技巧性地工作,必须检测交互式进程并提高其优先级,而必须"惩罚"非交互进程,以便是得系统获得更好的交互体验.这需要很多启发式的计算,他们可能完成的很好,也可能不工作
3.2 effective_prio设置动态优先级prio
可以通过函数effective_prio()用静态优先级static_prio计算动态优先级prio, 即·
p->prio = effective_prio(p);
该函数定义在kernel/sched/core.c, line 861
/*
* Calculate the current priority, i.e. the priority
* taken into account by the scheduler. This value might
* be boosted by RT tasks, or might be boosted by
* interactivity modifiers. Will be RT if the task got
* RT-boosted. If not then it returns p->normal_prio.
*/
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
/*
* If we are RT tasks or we were boosted to RT priority,
* keep the priority unchanged. Otherwise, update priority
* to the normal priority:
*/
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}
我们会发现函数首先effective_prio设置了普通优先级, 显然我们用effective_prio同时设置了两个优先级(普通优先级normal_prio和动态优先级prio)
因此计算动态优先级的流程如下
-
设置进程的普通优先级(实时进程99-rt_priority,普通进程为static_priority)
-
计算进程的动态优先级(实时进程则维持动态优先级的prio不变,普通进程的动态优先级即为其普通优先级)
最后, 我们综述一下在针对不同类型进程的计算结果
进程类型 | 实时优先级rt_priority | 静态优先级static_prio | 普通优先级normal_prio | 动态优先级prio |
---|---|---|---|---|
EDF调度的实时进程 | rt_priority | 不使用 | MAX_DL_PRIO-1 | 维持原prio不变 |
RT算法调度的实时进程 | rt_priority | 不使用 | MAX_RT_PRIO-1-rt_priority | 维持原prio不变 |
普通进程 | 不使用 | static_prio | static_prio | static_prio |
优先级提高的普通进程 | 不使用 | static_prio(改变) | static_prio | 维持原prio不变 |
3.2.1 为什么effective_prio使用优先级数值检测实时进程
rt_prio()会检测普通优先级是否在实时范围内,即是否小于MAX_RT_PRIO.参见include/linux/sched/rt.h#L6
static inline int rt_prio(int prio)
{
if (unlikely(prio < MAX_RT_PRIO))
return 1;
return 0;
}
而前面我们在normal_prio的时候, 则通过task_has_rt_policy来判断其policy属性来确定
policy == SCHED_FIFO || policy == SCHED_RR;
那么为什么effective_prio重检测实时进程是rt_prio基于优先级数值,而非task_has_rt_policy或者rt_policy?
对于临时提高至实时优先级的非实时进程(临时提高到实时优先级!!!)来说,这个是必要的,这种情况可能发生在是那个实时互斥量(RT-Mutex)时.
3.3 设置prio的时机
- 在新进程用wake_up_new_task唤醒时,或者使用nice系统调用改变其静态优先级时, 则会通过effective_prio的方法设置p->prio
wake_up_new_task(),计算此进程的优先级和其他调度参数,将新的进程加入到进程调度队列并设此进程为可被调度的,以后这个进程可以被进程调度模块调度执行。
- 进程创建时copy_process通过调用sched_fork来初始化和设置调度器的过程中会设置子进程的优先级
3.4 nice系统调用的实现
nice系统调用是的内核实现是sys_nice,其定义在kernel/sched/core.c#L7498,
它在通过一系列检测后,通过set_user_nice函数,其定义在kernel/sched/core.c#L3497
关于其具体实现我们会在另外一篇博客里面详细讲
3.5 fork时优先级的继承
在进程分叉处子进程时,子进程的静态优先级继承自父进程.子进程的动态优先级p->prio则被设置为父进程的普通优先级(!!!),这确保了实时互斥量(RT-Mutex)引起的优先级提高不会传递到子进程.
可以参照sched_fork函数,在进程复制的过程中copy_process通过调用sched_fork来设置子进程优先级,参见sched_fork函数
/*
* fork()/clone()-time setup:
*/
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
/* ...... */
/*
* Make sure we do not leak PI boosting priority to the child.
* 子进程的动态优先级被设置为父进程普通优先级
*/
p->prio = current->normal_prio;
/*
* Revert to default priority/policy on fork if requested.
* sched_reset_on_fork标识用于判断是否恢复默认的优先级或调度策略
*/
if (unlikely(p->sched_reset_on_fork)) /* 如果要恢复默认的调度策略, 即SCHED_NORMAL */
{
/* 首先是设置静态优先级static_prio
* 由于要恢复默认的调度策略
* 对于父进程是实时进程的情况, 静态优先级就设置为DEFAULT_PRIO
*
* 对于父进程是非实时进程的情况, 要保证子进程优先级不小于DEFAULT_PRIO
* 父进程nice < 0即static_prio < 的重新设置为DEFAULT_PRIO的重新设置为DEFAULT_PRIO
* 父进程nice > 0的时候, 则什么也没做
* */
if (task_has_dl_policy(p) || task_has_rt_policy(p))
{
p->policy = SCHED_NORMAL; /* 普通进程调度策略 */
p->static_prio = NICE_TO_PRIO(0); /* 静态优先级为nice = 0 即DEFAULT_PRIO*/
p->rt_priority = 0; /* 实时优先级为0 */
}
else if (PRIO_TO_NICE(p->static_prio) < 0) /* */
p->static_prio = NICE_TO_PRIO(0); /* */
/* 接着就通过__normal_prio设置其普通优先级和动态优先级
* 这里做了一个优化, 因为用sched_reset_on_fork标识设置恢复默认调度策略后
* 创建的子进程是是SCHED_NORMAL的非实时进程
* 因此就不需要绕一大圈用effective_prio设置normal_prio和prio了
* 直接用__normal_prio设置就可 */
p->prio = p->normal_prio = __normal_prio(p); /* 设置*/
/* 设置负荷权重 */
set_load_weight(p);
/*
* We don't need the reset flag anymore after the fork. It has
* fulfilled its duty:
*/
p->sched_reset_on_fork = 0;
}
/* ...... */
}
4 总结
task_struct采用了四个成员表示进程的优先级:prio和normal_prio表示动态优先级,static_prio表示进程的静态优先级.同时还用了rt_priority表示实时进程的优先级
字段 | 描述 |
---|---|
static_prio | 用于保存静态优先级, 是进程启动时分配的优先级, ,可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定 |
prio | 进程的动态优先级, 这个有显示才是调度器重点考虑的进程优先级 |
normal_prio | 普通进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级, 可以通过normal_prio来计算(非实时进程用static_prIo计算, 实时进程用rt_priority计算) |
rt_priority | 实时进程的静态优先级 |
调度器会考虑的优先级则保存在prio.由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用prio表示.由于这些改变不是持久的,因此静态优先级static_prio和普通优先级normal_prio不受影响.此外还用了一个字段rt_priority保存了实时进程的优先级静态优先级static_prio(普通进程)和实时优先级rt_priority(实时进程)是计算的起点, 通过他们计算进程的普通优先级normal_prio和动态优先级prio.
- 内核通过normal_prio函数计算普通优先级normal_prio
- 通过effective_prio函数计算动态优先级prio
5 参考
进程调度之sys_nice()系统调用
linux调度器源码研究 - 概述(一)
深入 Linux 的进程优先级