Linux 中进程的优先级绝不是如想象中的那么简单,相反它的概念比较混杂,它甚至不是很符合直觉。
Linux 进程的优先级跟随调度算法的不断发展,其意义在不同的阶段也有着不同的含义,所以本来想从 Linux 的调度发展史写起,但是无奈那一部分的涉猎不是很深入。不管怎样,发展到最后,结果是 Linux 系统可以在同一个系统上扩展多个调度算法,于是在同一个系统上面优先级也有了不同的含义,本文只对较新的 Linux 系统(截止4.4)的优先级做一个基本介绍,这个过程需要逐步深入,前面的一坨看起来与优先级不相关的东西并不是废话,别说话,耐心看。
字义上的优先级
所谓优先级就字面的意思来看就是事情的紧急程度,虽说有一个先来后到的说法,但是优先级则打破了这一规则。在可抢占的操作系统上面,高优先级的进程可以在后来者的情况下优先占据 cpu 的使用权,而更低优先级的进程则只能选择被动出让 cpu,无论其是否心甘情愿,也就是不管它的事情有咩有完成,都得让路给高优先级的进程(VIP)。
通常意义上来讲,我们平时说优先级也正是指的上面字面意义上的优先级,代表对 cpu 的优先使用级别,高优先级的进程先使用,低优先级的进程后使用。对于没有时间片限制的一些调度算法来说,只有最高优先级的进程主动让出 cpu(完成它自己的工作),次低一点优先级的进程才可以获取 cpu 使用权。而对于有时间片限制的调度算法,优先级高的进程则代表它优先、较多地享有 cpu 使用权,注意:优先、较多。它意味着可能不等到高优先级任务主动出让 cpu,就会被强制切换到更低一点优先级的进程,但是总体来说,高优先级的进程会有比其它进程更多的 cpu 使用时间。
Linux 系统的调度类
- 实时调度类
Linux 上面的实时调度不是硬实时的,而是尽可能的实时,比如SCHED_RR
、SCHED_FIFO
两种,采用 RT 调度算法。但是估计是这种实时调度策略只能够在某些特定的场景下发挥作用,而有一些对周期要求比较严格的场景,比如多媒体数据流,则无法满足。于是后续出台了更硬的实时调度策略,它就是SCHED_DEADLINE
,采用 EDF 调度算法,不过说实话,这种调度类我一直没能够驾驭得了,用上去效果总是不如预期,还是自己功力不足,有待进一步调教。 - 非实时调度类
就是大名鼎鼎的完全公平调度 CFS(Completely fair schedule)。在内核定义中是SCHED_OTHER
或者SCHED_NORMAL
,这两个代表同样的含义。还有SCHED_BATCH
与SCHED_IDLE
这两种,不过不是很常用,不过他们同属于 CFS 调度算法,也就是说它们的优先级概念是一样的。
Linux 上面的调度类主要就是这几个了,还有一类叫做 SCHED_ISO
,不过在内核里面作为保留字段,并没有实现。其中实时调度类使用 RT 调度算法,不过实时类的 SCHED_DEADLINE
调度类是 EDF 调度算法,非实时的调度类就是 CFS 调度算法了,可以推知,在 Linux 系统上面有三种优先级的概念,它们分别归属于不同的调度算法上,如下图所示:
内核角度看优先级
在内核里面 task_struct
用于描述进程的各个属性,属于进程的结构体抽象,其中有几个字段如下:
struct task_struct {
... ...
int prio, static_prio, normal_prio;
unsigned int rt_priority;
... ...
}
可以看到内核里面的优先级有一坨,分别记录为:
- prio
:动态优先级。,该值使用 effective_prio
函数进行计算,在 CFS 类或者 EDF 类的调度策略中与 normal_prio
是一致的,在 RT 策略当中就直接返回当前的动态优先级值(其值与静态优先级相同),该值主要是用于在运行过程中动态改变程序优先级的值,保证调度的公平性,低优先级的进程在长时间得不到运行状况下会暂时调高其动态优先级,这个计算过程比较复杂,这里不深入。
- static_prio
:静态优先级。只有 CFS 类的才有静态优先级,其他的类在内核当中也为其设置了该成员,但是并没有实际的物理意义,也就是静态优先级只有在 CFS 类中才有其实际的作用,其它的只是个幌子。该值的计算方式是通过一个宏定义来完成:#define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO)
,展开之后就是 nice + (100+(19-(-20)+1)/2)
。
- normal_prio
:归一化优先级。所谓归一化就是统一的意思,因为对于不同的调度类来说,它们的优先级的概念是不一样的,但是对于内核来说,它需要一个简单的概念来区分归根结底到底是谁的优先级更高一点。可以理解为不同的调度类优先级就是不同国家的货币,归一化优先级就是黄金,把不同国家的货币(不同调度类)转换为等量黄金,之后才能够比较到底谁才是土豪(谁拥有优先调度权)。
- rt_priority
:RT 调度算法类的优先级,其值与用户空间传进来的值一致(用户空间使用 sched_setparam
等函数传递),取值范围 1~99.
在内核的 INIT_TASK
宏定义中可以看到上面三个值初始化默认都是 MAX_PRIO-20
,也就是 120.后面有一张表列出了不同调度类情况下的归一化优先级的计算方法与值的范围。内核里面的归一化优先级值越小,代表实际优先级越高,目前最小的值是 -1
DEADLINE 调度类
该调度类在目前的 Linux 内核里面属于优先级最高的调度类,也就是说假设系统上面存在多个调度类,那么每次重新调度的时候,都是先从该调度类下面去找到一个可以投入运行的进程进行调度。归属于该调度类的进程其归一化优先级都是 -1,在该调度类内部不再区分多个优先级,统一归为 -1.
如果要切换某一个进程的调度类到 DEADLINE,那么就采用 sched_setattr
函数进行设置,大概的方式如下(由于本文章不讨论调度类的使用与原理,此处简略若干):
struct sched_attr attr;
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
/* This creates a 200ms / 1s reservation */
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 20*1000*1000;
attr.sched_deadline = attr.sched_period = 33*1000*1000;
ret = sched_setattr(0, &attr, flags);
内核里面关于该调度类优先级的一个宏定义是:MAX_DL_PRIO
,它的值是0,转换为归一化优先级就是 -1.目前为止,这个归一化优先级值的优先级最高。
FIFO、RR 等 RT 调度类
从内核的定义中可以看到宏定义:`
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
可以看到它的优先级最大是 100,转换之后就是 99(内核要减去1),也就是 RT 类型的调度类优先级范围在 0~99 之间,该调度类可以采用 sched_setparam
函数来完成优先级的设置,该函数的参数是 pid 号与一个叫做 sched_param
的结构体,该结构体只有一个参数,那就是优先级的值,该值越大(用户空间值越大优先级越高)表示优先级越高,范围是 1~99,需要注意的是从内核的角度来看归一化优先级的值越小,优先级才越高,也就是说用户在 user 空间设置的这个值是与内核里面的值是翻转着来的。
上面 task_struct->rt_priority
成员的值就是 sched_setparam
函数第二个参数的 sched_priority
成员传递的值。
CFS 调度类
主要就是 NORMAL/OTHER,BATCH 这两种调度类。它们的优先级可以通过 setpriority
函数来进行设置,可以设置的范围是 -20~19,需要注意的是这个范围指的是 nice 值,它与优先级有一定的区别(详情参考后面),分别对应内核里面的归一化优先级 100~139.
在 CFS 调度算法中,优先级并不会特别大地影响到程序被调用的先后顺序,在这种调度算法当中,优先级影响最大的是 cpu 使用时间,优先级越高可以占有的 cpu 使用时间就相对越多。所以如果拼命提高某些进程的优先级,最后发现它的实时性还不是很好,那不是因为优先级失效了,而是因为使用不当,要想提高程序的响应速度与实时性,最好是将它设置为 RT 调度类。
上述的有些调度类如果是在 ubuntu 桌面版上面做的实验,有些可能是不起作用的,因为 ubuntu 系统对非 root 用户的权限做了限制,比如优先级减少不能增加,调度类也不能随意更换(内核相关的选项没有被编译进去等),切换到 root 用户就可以正确地得到部分实验效果。
调度类的优先级
在内核里面,调度类的优先级顺序如下所示:
SCHED_DEADLINE > SCHED_FIFO/SCHEDRR > SCHED_NORMAl/SCHED_OTHER/SCHED_BATCH > SCHED_IDLE
区分一下调度类的优先级与进程的优先级,调度类包含了归属其下的进程,优先级高的调度类所包含的进程自然会被优先调度,然后在调度类内部才会进一步区分进程的优先级,其中优先级高的调度类中的进程总是会优先于优先级低的调度类中的进程被调度。比较绕,简单说调度顺序的选择有两个层级,调度类是第一层,进程是第二层。
对于使用同一种调度算法的调度类(RT、EDF、CFS),其调度类的优先级是一样的,实际调度中会在这几个调度类内部找到优先级比较高的进程去进行下一次调度,也就是优先级高的先调度。需要注意的是,就算是属于同一个调度算法的调度类,其实现上也有所不同,进程的调度行为也有所不同(比如 SCHED_RR 与 SCHED_FIFO 的调度行为有所不同),但是优先级的概念一定是统一的。
看下内核里面是怎样计算归一化优先级的
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_dl_policy(p))
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;
}
如果是 DEADLINE 调度类,那么它的归一化优先级就是 MAX_DL_PRIO-1
,也就是 -1,如果是实时的调度类,它的优先级是 MAX_RT_PRIO-1 - p->rt_priority
,范围是 0~98(没有 99 的原因是 rt_priority
的取值范围是 1~99),如果是 CFS 调度类,那么归一化优先级就和静态优先级是相等的,计算公式上面给出过,是一个叫 NICE_TO_PRIO
的宏定义完成计算的,这里再重复下:
#define MAX_NICE 19
#define MIN_NICE -20
#define NICE_WIDTH (MAX_NICE - MIN_NICE + 1)
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)
#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)
#define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO)
#define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO)
展开之后就是:静态优先级=nice值+MAX_USER_RT_PRIO+(MAX_NICE - MIN_NICE + 1)/2,带入计算一下就知道范围是 100~139(内核里面的 nice 取值范围是 -20~19).
调度类 | 内核归一化优先级范围 | user 可设置优先级/NICE范围 | 设置函数 | 调度算法 |
---|---|---|---|---|
SCHED_OTHER/NORMAL | 100~139 | -20~19 | sched_setpriority | CFS |
SCHED_IDLE | 100~139 | -20~19 | sched_setpriority | CFS |
SCHED_BATCH | 100~139 | -20~19 | sched_setpriority | CFS |
SCHED_RR | 0~98 | 99~1 | sched_setparam | RT |
SCHED_FIFO | 0~98 | 99~1 | sched_setparam | RT |
SCHED_DEADLINE | -1 | 不可设置 | sched_setparam | EDF |
可以注意到 RT 调度类有一个奇怪的问题,它的范围是 0~98,而不是 0~99,至于它的历史来由是什么我也不清楚,这里只是指出这个现象。另外前期 Linux 对不同的调度类极其优先级有一大堆的不同设置函数,从上表就可以看得出,后来在 DEADLINE 加入之后,估计是开发人员也受不了记忆这么多的函数,于是有了一个万能的函数:sched_setattr
,在系统里面可能找不到这个函数实现,使用 __NR_sched_setattr
系统调用号结合 syscall
就可以自行实现,该系统调用的第二个参数 sched_attr
如下所示(不解释了,很容易看懂它是如何统一各个调度类的设置的):
struct sched_attr {
u32 size;
u32 sched_policy;
u64 sched_flags;
/* SCHED_NORMAL, SCHED_BATCH */
s32 sched_nice;
/* SCHED_FIFO, SCHED_RR */
u32 sched_priority;
/* SCHED_DEADLINE */
u64 sched_runtime;
u64 sched_deadline;
u64 sched_period;
};
用户空间的 NI,PRI
除了内核空间里面繁杂、错综的优先级概念,在用户空间也有一些容易混淆的概念,它们之间有着某种联系,这里主要就说用户空间使用 htop 或者 top 命令看到的 NI 与 PRI 值是怎么一回事儿。
NI 就是 nice 值,顾名思义,就是这个进程的友好程度,越友好的程序 NI 值越大(取值范围 -20~19),也就越慷慨,它们会使用相对较少的 cpu 时间,所谓优先级低;而不那么友好的进程则使用相对较多的 cpu 时间,所谓优先级高。很怀疑起这个名字(nice)这是内核开发者的恶趣味,老实人就一定要慷慨,要付出更多,坏人就可以得到更多。由此可见,nice 值代表了可以使用的 cpu 资源的比例。
PRI 就是优先级,这个优先级不对应内核里面任何一个优先级的概念,可以看到它的值跟随 NI 值不断变化,在 CFS 调度时关系看起来就像是:PRI = 20+NI。实际上:PRI(new)=PRI(old)+nice,用户空间的进程刚初始化是默认都是 PRI=20,NI=0,当 NI 的值改变的时候,PRI 的值就显而易见。
在用户空间,我们想要设置某一个进程的 nice 值,那么这个进程一定是 CFS 调度算法下面的调度类,否则无意义,如果想让这个程序尽可能优先的运行,那么通过 nice 值修改可能并不会得到想象中的效果,更好的办法是提升进程的调度类,比如从 SCHED_OTHER
升级到 SCHED_RR
。如果想让自己的进程尽可能多地使用 cpu 时间,那么减少 nice 值就是一个很好的选择。
End
Linux 里面优先级绝不是那么简单就可以区分的,并且在使用的时候很可能会误用,需要针对内核角度、用户空间角度对优先级有一个全面的了解才能够更好地去使用它。本文的文字比较多,要想更加细致的理解就需要自己动手操作一番,在不同的调度类里面辗转腾挪一波,调调优先级,改改 nice 值就很容易理解这些概念了。
话说今天珠海刮台风,名曰「山竹」,然而它并不如听上去那么美好,刮起来真的是让人害怕.jpg,「山竹」大哥的级别达到了 17 级,比去年的「天鸽」姐姐 14 级更加暴躁,这一天还经历了断电断水,想必明天珠海肯定又是一片狼藉。我就很好奇,台风真的这么喜欢从珠海登陆?这有什么地理上的说法不。