发布时间:2023-06-18 15:30
nice值应该是熟悉Linux/UNIX的人很了解的概念了,我们都知它是反应一个进程“优先级”状态的值,其取值范围是-20至19,一共40个级别。这个值越小,表示进程”优先级”越高,而值越大“优先级”越低。我们可以通过nice命令来对一个将要执行的命令进行nice值设置,方法是:[root@zorrozou-pc0 zorro]#nice-n 10bash
。这样我就又打开了一个bash,并且其nice值设置为10,而默认情况下,进程的优先级应该是从父进程继承来的,这个值一般是0。我们可以通过nice命令直接查看到当前shell的nice值
[root@zorrozou-pc0 zorro]#nice
10
对比一下正常情况,打开一个正常的bash:
[root@zorrozou-pc0 zorro]#bash
[root@zorrozou-pc0 zorro]#nice
0
另外,使用renice命令可以对一个正在运行的进程进行nice值的调整,我们也可以使用比如top、ps等命令查看进程的nice值,具体方法我就不多说了,大家可以参阅相关manpage。
需要大家注意的是,我在这里都在使用nice值这一称谓,而非优先级(priority)这个说法。当然,nice和renice的man手册中, 也说的是priority这个概念,但是要强调一下,请大家真的不要混淆了系统中的这两个概念,一个是nice值,一个是priority值,他们有着千 丝万缕的关系,但对于当前的Linux系统来说,它们并不是同一个概念。
我们看这个命令:
[root@zorrozou-pc0 zorro]#ps-l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0692457760800-17952 poll_s pts/500:00:00sudo
4 S 0692569240800-4435 wait pts/500:00:00bash
0 R 01297169250800-8514- pts/500:00:00ps
同样的,如果是top命令:
Tasks:1587 total,7 running,1570 sleeping,0 stopped,10 zombie
Cpu(s):13.0%us,6.9%sy,0.0%ni,78.6%id,0.0%wa,0.0%hi,1.5%si,0.0%st
Mem:132256952k total,107483920k used,24773032k free,2264772k buffers
Swap:2101192k total,508k used,2100684k free,88594404k cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3001 root 200232m21m4500 S 12.90.00:15.09 python
11541 root 200174562400888 R 7.40.00:00.06top
大家是否搞清楚了这其中PR值和NI值的差别?如果没有,那么我们可以首先搞清楚什么是nice值。
nice值虽然不是priority,但是它确实可以影响进程的优先级。
在英语中,如果我们形容一个人nice,那一般说明这个人的人缘比较好。什么样的人人缘好?往往是谦让、有礼貌的人。比如,你跟一个nice的人一 起去吃午饭,点了两个一样的饭,先上了一份后,nice的那位一般都会说:“你先吃你先吃!”,这就是人缘好,这人nice!但是如果另一份上的很晚,那 么这位nice的人就要饿着了。这说明什么?越nice的人抢占资源的能力就越差,而越不nice的人抢占能力就越强。这就是nice值大小的含义,nice值越低,说明进程越不nice,抢占cpu的能力就越强,优先级就越高。在原来使用O1调度的Linux上,我们还会把nice值叫做静态优先级,这也基本符合nice值的特点,就是nice值设定好了之后,除非我们用renice去改它,否则它是不变的。而priority的值在之前内核的O1调度器上表现是会变化的,所以也叫做动态优先级。
简单了解nice值的概念之后,我们再来看看什么是priority值,就是ps命令中看到的PRI值或者top命令中看到的PR值。本文为了区分这些概念,以后统一用nice值表示NI值,或者叫做静态优先级,也就是用nice和renice命令来调整的优先级;而使用priority值表示PRI和PR值,或者叫动态优先级。我们也统一将“优先级”这个词的概念规定为表示priority值的意思。
在内核中,进程优先级的取值范围是通过一个宏定义的,这个宏的名称是MAX_PRIO,它的值为140。而这个值又是由另外两个值相加组成的,一个 是代表nice值取值范围的NICE_WIDTH宏,另一个是代表实时进程(realtime)优先级范围的MAX_RT_PRIO宏。说白了就 是,Linux实际上实现了140个优先级范围,取值范围是从0-139,这个值越小,优先级越高。nice值的-20到19,映射到实际的优先级范围是 100-139。新产生进程的默认优先级被定义为:
#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH /2)
实际上对应的就是nice值的0。正常情况下,任何一个进程的优先级都是这个值,即使我们通过nice和renice命令调整了进程的优先级,它的 取值范围也不会超出100-139的范围,除非这个进程是一个实时进程,那么它的优先级取值才会变成0-99这个范围中的一个。这里隐含了一个信息,就是 说当前的Linux是一种已经支持实时进程的操作系统。
内核在设计的时候,将实时进程单独映射了100个优先级,这些优先级都要高与正常进程的优先级(nice值),而实时进程的调度算法也不同,它们采用更简单的调度算法来减少调度开销。所有优先级值在0-99范围内的,都是实时进程,所以这个优先级范围也可以叫做实时进程优先级,100-139范围内的是非实时进程。在系统中可以使用chrt命令来查看、设置一个进程的实时优先级状态。我们可以先来看一下chrt命令的使用:
[root@zorrozou-pc0 zorro]# chrt
Showor change the real-time scheduling attributes of a process.
Set policy:
chrt [options]<priority><command>[<arg>...]
chrt [options]-p <priority><pid>
Get policy:
chrt [options]-p <pid>
Policy options:
-b,--batch set policy to SCHED_OTHER
-f,--fifo set policy to SCHED_FIFO
-i,--idle set policy to SCHED_IDLE
-o,--other set policy to SCHED_OTHER
-r,--rr set policy to SCHED_RR (default)
Scheduling flag:
-R,--reset-on-fork set SCHED_RESET_ON_FORK for FIFO or RR
Other options:
-a,--all-tasks operate on all the tasks (threads)for a given pid
-m,--max show min and max valid priorities
-p,--pid operate on existing given pid
-v,--verbose display status information
-h,--help display this help andexit
-V,--version output version information andexit
Formore details see chrt(1).
我们先来关注显示出的Policy options部分,会发现系统给个种进程提供了5种调度策略。但是这里并没有说明的是,这五种调度策略是分别给两种进程用的,对于实时进程可以用的调度 策略是:SCHED_FIFO、SCHED_RR,而对于非实时进程则是:SCHED_OTHER、SCHED_OTHER、SCHED_IDLE。
系统的整体优先级策略是:如果系统中存在需要执行的实时进程,则优先执行实时进程。直到实时进程退出或者主动让出CPU时,才会调度执行非实时进程。实时进程可以指定的优先级范围为1-99,将一个要执行的程序以实时方式执行的方法为:
[root@zorrozou-pc0 zorro]# chrt 10bash
[root@zorrozou-pc0 zorro]# chrt -p $$
pid 14840's current scheduling policy: SCHED_RR
pid 14840's current scheduling priority:10
可以看到,新打开的bash已经是实时进程,默认调度策略为SCHED_RR,优先级为10。如果想修改调度策略,就加个参数:
[root@zorrozou-pc0 zorro]# chrt -f 10bash
[root@zorrozou-pc0 zorro]# chrt -p $$
pid 14843's current scheduling policy: SCHED_FIFO
pid 14843's current scheduling priority:10
刚才说过,SCHED_RR和SCHED_FIFO都是实时调度策略,只能给实时进程设置。对于所有实时进程来说,优先级高的(就是 priority数字小的)进程一定会保证先于优先级低的进程执行。SCHED_RR和SCHED_FIFO的调度策略只有当两个实时进程的优先级一样的时候才会发生作用,其区别也是顾名思义:
这就是Linux对于实时进程的优先级和相关调度算法的描述。整体很简单,也很实用。
对于非实时进程优先级的处理相对比较麻烦,它们才是Linux上进程的主要分类,首先还是要来介绍一下它们相关的调度算法:O1和CFS。
O1调度器之所以叫做O1,主要是因为其算法的时间复杂度是O1。其仍然是根据经典的时间片分配的思路来进行整体设计的。简单来说,时间片的思路就是将CPU的执行时间 分成一小段一小段的,假如是5ms一段。于是多个进程如果要“同时”执行,实际上就是每个进程轮流占用5ms的cpu时间,而从1s的时间尺度上看,这些进程就是在“同时”执行的。当然,对于多核系统来说,就是把每个核心都这样做就行了。而在这种情况下,如何支持优先级呢?实际上就是将时间片分配成大小不等的若干种,优先级高的进程使用大的时间片,优先级小的进程使用小的时间片。这样在一个周期结速后,优先级大的进程就会占用更多的时间而因此得到特殊待遇。
O1算法还有一个比较特殊的地方是,即使是相同的nice值的进程,也会再根据其CPU的占用情况将其分成两种类型:CPU消耗型和IO消耗型。 典型的CPU消耗型的进程的特点是,它总是要一直占用CPU进行运算,分给它的时间片总是会被耗尽之后,程序才可能发生调度。比如常见的各种算数运算程序。而IO消耗型的特点是,它经常时间片没有耗尽就自己主动先释放CPU了,比如vi,emacs这样的编辑器就是典型的IO消耗型进程。为了提高IO消耗型进程的响应速度,系统将动态调整CPU消耗的进程将其优先级降低,而IO消耗型的进程的优先级将变高。已知nice值的范围是-20 - 19,其对应priority值的范围是100-139,对于一个默认nice值为0的进程来说,其初始priority值应该是120,随着其不断执行,内核会观察进程的CPU消耗状态,并动态调整priority值,可调整的范围是±5。就是说,最其优先级可以呗自动调整到115,最低到 125。这也是为什么nice值叫做静态优先级而priority值叫做动态优先级的原因。不过这个动态调整的功能在调度器换成CFS之后就不需要了,因为CFS换了另外一种CPU时间分配方式,这个我们后面再说。
再简单了解了O1算法按时间片分配CPU的思路之后,我们再来结合进程的状态简单看看其算法描述。我们都知道进程有5种状态:
S(Interruptible sleep):可中断休眠状态。
D(Uninterruptible sleep):不可中断休眠状态。
R(Running or runnable):执行或者在可执行队列中。
Z(Zombie process):僵尸。
T(Stopped):暂停。
在CPU调度时,主要只关心R状态进程,因为其他状态进程并不会被放倒调度队列中进行调度。调度队列中的进程一般主要有两种情况,一种是进程已经被调度到CPU上执行,另一种是进程正在等待被调度。出现这两种状态的原因应该好理解,因为需要执行的进程数可能多于硬件的CPU核心数,比如需要执行的进程有8个而CPU核心只有4个,此时cpu满载的时候,一定会有4个进程处在“等待”状态,因为此时有另外四个进程正在占用CPU执行。
根据以上情况我们可以理解,系统当下需要同时进行调度处理的进程数(R状态进程数)和系统CPU的比值,可以一定程度的反应系统的“繁忙”程度。需要调度的进程越多,核心越少,则意味着系统越繁忙。除了进程执行本身需要占用CPU以外,多个进程的调度切换也会让系统繁忙程度增加的更多。所以,我们往 往会发现,R状态进程数量在增长的情况下,系统的性能表现会下降。系统中可以使用uptime命令查看系统平均负载指数(load average):
[zorro@zorrozou-pc0 ~]$ uptime
16:40:56 up 2:12,1 user, load average:0.05,0.11,0.16
其中load average中分别显示的是1分钟,5分钟,15分钟之内的平均负载指数,可以简单认为是相映时间范围内的R状态进程个数。但是这个命令显示的数字是绝对个数,并没有表示出不同CPU核心数的实际情况。比如,如果我们的1分钟load average为16,而CPU核心数为32的话,那么这个系统的其实并不繁忙。但是如果CPU个数是8的话,那可能就意味着比较忙了。但是实际情况往往 可能比这更加复杂,比如进程消耗类型也会对这个数字的解读有影响。总之,这个值的绝对高低并不能直观的反馈出来当前系统的繁忙程度,还需要根据系统的其它指标综合考虑。
O1调度器在处理流程上大概是这样进行调度的:
CFS是一般优先级(SCHED_OTHER)进程的调度方法。在这个重新设计的调度器中,时间片,动态、静态优先级以及IO消耗,CPU消耗的概念都不再重要。其设计的基本思路是,我们想要实现一个对所有进程完全公平的调度器。又是那个老问题:如何做到完全公平?答案跟上一篇IO调度中CFQ的思路类似: 如果当前有n个进程需要调度执行,那么调度器应该再一个比较小的时间范围内,把这n个进程全都调度执行一遍,并且它们平分cpu时间,这样就可以做到所有进程的公平调度。那么这个比较小的时间就是任意一个R状态进程被调度的最大延时时间,即:任意一个R状态进程,都一定会在这个时间范围内被调度响应,这个时间也可以叫做调度周期。进程越多,每个进程在周期内被执行的时间就会被平分的越小。调度器只需要对所有进程维护一个累积占用CPU时间数,就可以衡量出每个进程目前占用的CPU时间总量是不是过大或者过小,这个数字记录在每个进程的vruntime 中。所有待执行进程都以vruntime为key放到一个由红黑树组成的队列中,每次被调度执行的进程,都是这个红黑树的最左子树上的那个进程,即 vruntime时间最少的进程,这样就保证了所有进程的相对公平。
在基本驱动机制上CFS跟O1一样,每次时钟中断来临的时候,都会进行队列调度检查,判断是否要进程调度。当然还有别的时机需要调度检查,发生调度的时机可以总结为这样几个:
当然,CFS中还需要支持优先级。在新的体系中,优先级是以时间消耗(vruntime增长)的快慢来决定的。就是说,对于CFS来说不同优先级进程的vruntime增长的比率是不同的,高优先级进程时间增长的慢,低优先级时间增长的快。比如,优先级为19的进程,实际占用cpu为1秒,那么在vruntime中就记录1s。但是如果是-20优先级的进程,那么它很可能实际占CPU用 10s,在vruntime中才会纪录1s。CFS真实实现的不同nice值的cpu消耗时间比例在内核中是按照“每差一级cpu占用时间差10%左右” 这个原则来设定的。这里的大概意思是说,如果有两个nice值为0的进程同时占用cpu,那么它们应该每人占50%的cpu,如果将其中一个进程的 nice值调整为1的话,那么此时应保证优先级高的进程比低的多占用10%的cpu,就是nice值为0的占55%,nice值为1的占45%。那么它们占用cpu时间的比例为55:45。这个值的比例约为1.25。就是说,相邻的两个nice值之间的cpu占用时间比例的差别应该大约为1.25。根据这个原则,内核对40个nice值做了时间计算比例的对应关系,它在内核中以一个数组存在:
staticconstint prio_to_weight[40]={
/* -20 */88761,71755,56483,46273,36291,
/* -15 */29154,23254,18705,14949,11916,
/* -10 */9548,7620,6100,4904,3906,
/* -5 */3121,2501,1991,1586,1277,
/* 0 */1024,820,655,526,423,
/* 5 */335,272,215,172,137,
/* 10 */110,87,70,56,45,
/* 15 */36,29,23,18,15,
};
根据CFS的特性,我们知道调度器总是选择vruntime最小的进程进行调度。那么如果有两个进程的初始化vruntime时间一样时,一个进程 被选择进行调度处理,那么只要一进行处理,它的vruntime时间就会大于另一个进程,CFS难道要马上换另一个进程处理么?出于减少频繁切换进程所带来的成本考虑,显然并不应该这样。CFS设计了一个sched_min_granularity_ns参数,用来设定进程被调度执行之后的最小CPU占用 时间。
[zorro@zorrozou-pc0 ~]$ cat/proc/sys/kernel/sched_min_granularity_ns
2250000
一个进程被调度执行后至少要被执行这么长时间才会发生调度切换。
我们知道无论到少个进程要执行,它们都有一个预期延迟时间,即:sched_latency_ns,系统中可以通过如下命令来查看这个时间:
[zorro@zorrozou-pc0 ~]$ cat/proc/sys/kernel/sched_latency_ns
18000000
在这种情况下,如果需要调度的进程个数为n,那么平均每个进程占用的CPU时间为sched_latency_ns/n。显然,每个进程实际占用的 CPU时间会因为n的增大而减小。但是实现上不可能让它无限的变小,所以sched_min_granularity_ns的值也限定了每个进程可以获得 的执行时间周期的最小值。当进程很多,导致使用了sched_min_granularity_ns作为最小调度周期时,对应的调度延时也就不在遵循 sched_latency_ns的限制,而是以实际的需要调度的进程个数n * sched_min_granularity_ns进行计算。
CFS是通过vruntime最小值来选择需要调度的进程的,那么可以想象,在一个已经有多个进程执行了相对较长的系统中,这个队列中的 vruntime时间纪录的数值都会比较长。如果新产生的进程直接将自己的vruntime值设置为0的话,那么它将在执行开始的时间内抢占很多的CPU 时间,直到自己的vruntime追赶上其他进程后才可能调度其他进程,这种情况显然是不公平的。所以CFS对每个CPU的执行队列都维护一个 min_vruntime值,这个值纪录了这个CPU执行队列中vruntime的最小值,当队列中出现一个新建的进程时,它的初始化vruntime将不会被设置为0,而是根据min_vruntime的值为基础来设置。这样就保证了新建进程的vruntime与老进程的差距在一定范围内,不会因为 vruntime设置为0而在进程开始的时候占用过多的CPU。