该文章参考宋宝华老师的进程视频课程,详细可以去听阅码场宋老师的课程。
CPU/IO消耗型进程
-
吞吐率 vs. 响应
-
SCHED_FIFO、SCHED_RR
-
SCHED_NORMAL和CFS
-
nice、renice
-
chrt
-
rt_period_us和rt_runtime_us配置
1、 吞吐 和 响应
我们在思考一个系统的调度器时,要理解任何操作系统的调度器设计只追求两个目标:吞吐率大 和 响应快(低延时) 。但是这两个指标是相对的,吞吐率高必然导致高延迟(响应慢)。
这就像马路上给消防车,救护车让路,消防车救护车的效率高了,但是整个马路的效率低了。
如果不抢占,吞吐会最好,但是响应就很差。
吞吐率大:势必要把更多的时间花费在 真实的有用功 上,而不是把时间浪费在频繁的 进程上下文切换 上。
低延时(响应快):要求把优先级高的进程可以随时抢占进来,打断别人,强行插队。但是抢占会引起上下文切换,上下文切换的时间,本身对吞吐率来说,是一个消耗,这个消耗可以降低到 2us 或者更低(这看起来没什么?),但是上下文切换更大的消耗不是切换本身,而是切换会引起大量的cache miss。你明明weibo跑的很爽,现在切过去微信,那么CPU的cache是不太容易命中微信的。
2、 IO消耗型 vs CPU消耗型 进程
一般来说,IO消耗型的进程 优先级比较高,因为一般和用户体验相关的都是IO消耗型的进程,比如用户敲键盘,或者鼠标,而不是正在编译Android的进程。而CPU消耗型的优先级比较低。
IO消耗型进程:对 CPU的本身的性能不敏感,但是对及时抢占到CPU很敏感。
CPU消耗型进程:对 CPU的本身的性能敏感,对及时抢占到CPU也很敏感。
举个例子,比如要从硬盘上读取4k的数据,
所以现在有很多ARM的处理器 ,都是 big.little 大核和小核
原来的都是叫做big.LITTLE,现在的是叫做 DynamIQ big.LITTLE
调度算法
最常用的就是三种,
在 内核 里面,将优先级分为 0 - 139,数字越小,优先级越高(用户层相反)。
其中 0 - 99 是给RT使用的,
100 - 139 是给 normal 普通进程线程 使用的,
对于普通的进程而言,100 - 139 ,是比较善良的,我们一般不说优先级,一般用 NICE 这个概念来形容,nice值 (-20 19),形容一个人干得好,干的不好。 比如坐地铁,有人经常给老人让座,那么他的nice 值就大,不让座的人 nice 值就小。普通的进程 都是比较善良的,比如 nice 值为 -20的有20块钱,看到有个nice 值为 -16的,就给了他10块钱,nice值 -16的 看到路边有个流浪汉 nice值为-10 ,就给了他4.5块钱......
Documentation/scheduler/sched-nice-design.txt
nice值小的,获得的时间片多,nice值大的获得的 时间片少 (人好,给人让座了)
动态的奖励和惩罚:比如给你分配了 100ms 的时间片,你每次都用完了,说明你很忙,很喜欢干活,是个CPU消耗型,linux 就会对你进行惩罚,如果你还是喜欢干活,就继续惩罚,nice值越来越低(优先级高)。 反之,给你分配了100ms时间片,你只用了30ms, 说明你可能是一个IO消耗型,喜欢闲着,linux 就会对你奖励,nice 值越来越大(优先级低)。
对于优先级0-99 只受 RR FIFO的约束,和 nice 没有关系,只有 普通进程 才受 nice的约束。
上面就是早期 2.6的调度策略,现在虽然加了很多补丁,算法改了,但是大致原理是相似的。
在 1s 内,rt 进程最多只能跑 0.95s , 剩下的 0.05s 让普通人也喝点汤,也能避免RT进程有bug, 把系统挂死了。这个就是 RT进程的熔断机制。
RT进程一般都很少很少,所以给他分配了 0.95s 已经很好了,要是RT进程都很多,还谈什么RT呢?路上都是救护车,警车,那效率一样很低啊。
注意应用层设置的优先级 0-99 数字越大,优先级越高。这个只针对 FIFO 和 RR策略。
也可以通过 命令来修改 一些进程的 调度策略 优先级
举个例子,CPU两个核,现在跑一个普通的多线程的程序,创建两个线程,默认nice值都是0 ,
主线程, 等待回收线程,不占CPU,其余两个线程,都是while(1) 狂占CPU
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
void *thread_fun(void *param)
{
printf("thread pid:%d, tid:%lu\n", getpid(), pthread_self());
while (1) ;
return NULL;
}
int main(void)
{
pthread_t tid1, tid2;
int ret;
printf("main pid:%d, tid:%lu\n", getpid(), pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return 1;
}
ret = pthread_create(&tid2, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return 1;
}
if (pthread_join(tid1, NULL) != 0) {
perror("call pthread_join function fail");
return 1;
}
if (pthread_join(tid2, NULL) != 0) {
perror("call pthread_join function fail");
return 1;
}
return 0;
}
现在跑起来,是一个普通进程,是没有 RT的熔断机制的。
top 看下,等top稳定之后,看 a.out 的CPU利用率,大概会稳定在 200%
不过此时要是,用鼠标拖动窗口,会发现 a.out 的利用率会下降,这说明,有优先级更高的桌面程序抢占到cpu了,所以 a.out 的利用率会下降。要是不拖动窗口,top稳定之后,还是会在200%左右。
执行下面的命令 将 a.out 设置优先级高一点,-a 是把该进程的所有线程都设置,-p 指优先级,-f 是 fifo 策略,最后跟的是pid
chrt -f -a -p 50 4507
执行 这条命令后,会将 a.out 设为RT进程了,FIFO策略,是更牛逼了,此时 a.out 的 CPU利用率会降下来,这是为什么?
为什么会稳定在 160% 呢?
这是因为 设置了 RT进程的熔断机制, 1s 最多跑 0.8s的RT进程,两核的CPU
不过此时a.out 变为更牛逼的RT进程之后呢,桌面的用户体验就不太好了,拖动窗口就很慢,因为此时桌面程序的优先级 没有 a.out 高了,抢不过了a.out了。
下面有三个进程,两个RT的进程,一个普通进程,但是他们的CPU占用率如下图 很奇怪,
其实这是由于RT进程的熔断效应造成的,P2 跑不过 P3 只能怪 优先级没有 P1高,和 P3 没有关系。
现在的内核,普通进程 的调度算法是 CFS 调度算法
红黑树,是一种二叉树,所有左边的节点,都小于右边的节点。内核 将所有的 普通进程都挂在一颗红黑树上面,挂在上面的不是pid ,也不是 nice值,而是进程的虚拟时间vruntime 。
完全公平调度 :所有的进程追求一个虚拟时间的公平。
虚拟时间怎么计算呢?进程运行一段时间之后,虚拟时间就等于,以前的虚拟时间 + 一个比重,见下图。
delta 是进程运行的物理时间(进程在CPU上跑了1s), NICE_0_LOAD 代表 nice=0的权重,就是 1024 ,se.weight 是进程本身的权重。
所以,对于 nice=0 的进程而言,虚拟时间 就 是 进程运行的物理时间,因为 se.weight = 1024
但是对于那些 nice 比较低的进程,它的权重就会比较大,分母很大,就容易在树的左边,就容易被调度到。要想让 vruntime 长的慢,要么分母大(nice值小的),要么分子小(喜欢睡的进程,IO进程),就容易调度的到。
下面跑一个例子,还是上面的程序 a.out 普通进程。
后台执行两个a.out 由于两个进程都是 普通进程,nice=0 所以公平的划分 CPU ,都是接近 100%的利用率 (两个核)
根据 CFS 完全公平调度算法,要追求所有 红黑树上的 虚拟时间的完全相等,现在将其中一个 a.out 的 nice 值 调整为 -5 ,根据下图的权重可知,nice=-5的权重比 nice=0的权重大了三倍,也就是分母大了三倍,要想保证虚拟时间相等,只有将 nice= -5 的 分子 也增大三倍,也就是给 nice = -5 的进程 3倍的时间,
使用 下面的命令调整, -n 指定 nice 值,-g 所有的线程
renice -n -5 -g 3971
可以看出来,调整后的CPU占用率是 3:1, 他们加起来还是将近 200% (两核)
此时,拖到窗口,CPU占用率就会下来,因为桌面程序IO消耗性 一般优先级更高。
注意: CFS 只针对普通进程,和RT进程没关系,RT进程只在 bit 图中寻找第一个bit=1的进程跑(优先级最高的)。
Linux 当中,绝大多数进程都是 普通进程。