- 学习交流加(可免费帮忙下载CSDN资源):
- 个人微信: liu1126137994
- 学习交流资源分享qq群1(已满): 962535112
- 学习交流资源分享qq群2: 780902027
上一篇文章(点击链接:【Linux进程、线程、任务调度】二)讲了:
- fork vfork clone 的含义
- 写时拷贝技术
- Linux线程的实现本质
- 进程0 和 进程1
- 进程的睡眠和等待队列
- 孤儿进程的托孤 ,SUBREAPER
本篇文章接着上一篇文章记录以下学习内容:
- CPU/IO消耗型进程
- 吞吐率 vs. 响应
- SCHED_FIFO算法 与 SCHED_RR算法
- SCHED_NORMAL算法 和 CFS算法
- nice与renice
- chrt
本篇文章主要讲解Linux系统调度器。对于调度器来说,我们需要考虑的有:吞吐与响应,CPU消耗型与IO消耗型进程。这四点都是对于调度算法的输入来说的。
同时,调度器的单位是线程,但是线程,我们知道在第一篇文章中已经讲解了,线程是一种轻量级的进程。所以本篇文章在说调度的对象时说的都是进程,但是我们要理解,线程才是调度单位。这并不矛盾!!!
1、吞吐 vs. 响应
吞吐和响应之间的矛盾
- 响应:最小化某个任务的响应时间,哪怕以牺牲其他任务为代价
- 吞吐:全局视野,整个系统的workload被最大化处理
首先我们在考虑调度器的时候,我们要理解操作系统的调度器设计目标追求两点:吞吐率大和延迟低。
这两点是相互矛盾的。因为吞吐率大,势必要把更多的时间放到真实有用功上,而不是把时间浪费在进程上下文切换上。而延迟低,势必要求优先级高的进程可以随时抢占进来,打断别人,强行插队。但是上下文切换的时间,对吞吐率来讲,本身是一个消耗。花了很多时间在上下文切换上,相当于做了很多无用功。这种消耗可以低到2us或者更低(这看起来没什么?),但是上下文切换更大的消耗不是切换本身,而是切换导致的cache miss。本身跑微博跑的好好的,现在要切换过去跑微信,CPU的cache,是很难命中微信的。
不抢占,肯定响应差,抢了又会导致吞吐率下降。
Linux系统不是一个完全照顾吞吐的系统,也不是一个完全照顾相应的的系统。它作为一个软实时系统,实际上是想达到某种平衡(后面会讲)。同时也提供给用户一定的配置能力。在内核编译的时候,Kernel Features —> Preemption Model选项实际上可以让我们编译内核的时候,是倾向于支持吞吐,还是支持响应:
越往上面选,吞吐越好,越好下面选,响应越好。服务器你一个月也难得用一次鼠标,而桌面则显然要求一定的响应,这样可以保证UI行为的表现较好。但是Linux即便选择的是最后一个选项“Preemptible Kernel (Low-Latency Desktop)”,它仍然不是硬实时的(下一篇文章讲解为何Linux系统不是一个硬实时系统)。
2、IO消耗型 vs. CPU消耗型
进程分为:IO消耗型进程与CPU消耗型进程
- IO消耗型(狂睡,等IO资源等):CPU利用率低,进程的运行效率主要受限于I/O速度
- CPU消耗型(狂算):多数时间花在CPU上面(做运算)
一般而言,IO消耗型任务对延迟比较敏感,应该被优先调度。它虽然时间都花在IO上,不关心CPU的性能,但是它关心的是是否能够及时的拿到CPU的使用权。也就是是否可以及时的被CPU调度。当自己完成了IO操作,但是一直不被CPU调度,那肯定也是不行的。比如,你正在疯狂编译安卓系统,而等鼠标行为的用户界面老不工作(正在狂睡),但是鼠标一点,我们应该优先打断正在编译的进程,而去响应鼠标这个I/O,这样电脑的用户体验才符合人性。
3、实时进程调度
在早期2.6内核时,调度器使用的是优先级数组和Bitmaps
- 优先级号一共有0-139,其中0-99的是RT(实时)进程,100-139的是非实时进程。
- 某个优先级有TASK_RUNNING进程,响应bit设置为1.
- 调度第一个bitmap设置为1的进程。
对于Linux的RT进程,按照SCHED_FIFO和SCHED_RR的策略。
- SCHED_FIFO:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑。同等优先级的先进先出,先ready的跑到睡,后ready的接着跑。
- SCHED_RR:不同优先级按照优先级高的先跑到睡眠,优先级低的再跑。同等优先级的进行时间片轮转。
比如Linux存在如下4个进程,T1~T4(内核里面优先级数字越低,优先级越高):
那么它们在Linux的跑法就是:
当然,Linux中大多数的进程都不是RT的,而是非RT的普通进程。并且,就算有RT的进程一直存在,CPU也不会一直被RT进程霸占,必须给普通进程留有一定的时间片。在Linux中存在一个RT门限。
在sched_rt_period_us时间内,RT进程最多跑sched_rt_runtime_us时间,剩下的时间必须留给非RT进程使用。
在Linux系统中上述两个时间在如下位置:
/proc/sys/kernel/sched_rt_period_us
/proc/sys/kernel/sched_rt_runtime_us
4、非实时进程的调度
4.1 早期2.6内核:
- 在不同的优先级进行时间片轮转
- -20 ~ 19的nice值
- 根据睡眠情况,动态奖励和惩罚
普通进程调度,进程不会像RT进程那样一致霸占CPU,而是所有的进程都轮转获得CPU,只是当优先级高的话,可获得更多的时间片,醒来后可以抢占优先级低的进程。
但是,当进程的CPU占有率高,或者一开始的优先级高的话,后面内核会降低它的优先级,这样可以让IO消耗型的进程能够竞争过CPU消耗型的进程。
从而保障IO消耗型的进程能够及时获得CPU使用权。
4.2 CFS完全公平调度策略
新内核采用的调度策略就不是那么简单了,为了很好的统一CPU消耗型与IO消耗型进程的调度,与优先级(nice值)的协调,新内核采用了一种策略:完全公平调度策略。
注意:完全公平调度策略是针对普通进程而言的。
完全公平调度策略,内部的实现使用的是红黑树。左边节点的值小于右边节点的值。
红黑树节点的值为vruntime,进程的虚拟运行时间。
vruntime = pruntime * NICE_0_LOAD/ weight
- pruntime:进程的物理运行时间,即实际的运行时间
- weight:权重
- NICE_0_LOAD:参数,等于1024,也是nice值为0的权重
nice值与weight值的对应关系:
CFS调度策略:
当RT进程都睡眠了(或者RT进程已经跑了超过了sched_rt_runtime_us时间值),那么就该普通进程被调度了。Linux最先调度vruntime最小的进程,也就是位于红黑树最左边的进程。假设最先调度的进程是p1,那么随着p1的运行,p1的pruntime就会变大,就会导致p1的vruntime就会变大,那么p1在红黑树中的位置就会往右移动。下一次,就会调度最新的vruntime最小的进程(最新的红黑树最左边的进程)。
那么什么样的线程最容易被调度呢?由上述公式知,当pruntime小,weight值大的时候,vruntime小,最容易被调度到。而pruntime小意味着是IO消耗型进程,weight值大的意味着是nice值小(优先级高)的进程。
这样的话,我们就可以看到,红黑树很神奇的同时照顾了普通进程的CPU/IO消耗型与优先级(nice值)的情况。
比如有4个普通进程,如下表,目前显然T1的vruntime最小(这是它喜欢睡的结果),然后T1被调度到。
pruntime | Weight | vruntime | |
---|---|---|---|
T1 | 8 | 1024(nice=0) | 8*1024/1024=8 |
T2 | 10 | 526 (nice=3) | 10*1024/526=19 |
T3 | 20 | 1024(nice=0) | 20*1024/1024=20 |
T4 | 20 | 820 (nice=1) | 20*1024/820=24 |
然后,我们假设T1被调度再执行12个pruntime,它的vruntime将增大delta*1024/weight(这里delta是12,weight是1024),于是T1的vruntime成为20,那么这个时候vruntime最小的反而是T2(为19),此后,Linux将倾向于调度T2(尽管T2的nice值大于T1,优先级低于T1,但是它的vruntime现在只有19)。
所以普通进程的调度,是一个综合考虑IO/CPU消耗型与优先级的,通俗点说就是考虑你喜欢睡还是喜欢干活,以及你的nice值是多少(优先级高不高)。所以,Linux中进程的调度时间是不确定的,它具有随机性,无法判断一个进程的调度的延迟,更无法判断一个进程什么时候会被调度到,它是需要看看当前系统中是否还有其他进程在跑,以及被唤醒的进程的nice值以及它之前喜不喜欢睡觉!!!
还有一点要注意:普通进程在跑,如果突然有一个RT进程过来了,那么RT进程就是无敌的,它会被调度,直到它运行结束或者睡眠。
5、工具chrt和renice
chrt工具可以设置进程的调度策略与优先级,nice和renice可设置进程的nice值。renice是程序已经跑起来了你可以去设置nice值。
看一下程序:
two-loops.c
#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;
}
-
编译并在后天运行两份:
$ gcc two-loops.c -pthread
top命令查看CPU利用率:
可以看到,运行的两个程序的CPU利用率都接近百分之百。
renice其中之一,再观察CPU利用率
可以很明显的看到,renice修改了进程的优先级,导致进程的CPU占有率变了。
- 编译并在后台运行一份
top查看器CPU利用率接近百分之200:
把它所有的线程设置为SCHED_FIFO调度策略,优先级为50:
$ chrt -f -p 50 3110
- -f代表FIFO,-p代表进程pid 50为设置的优先级
然后会发现进程的CPU利用率会降低一些。
当然设置调度策略(SCHED_FIFO)和RT优先级,除了可以用chrt工具,还可以直接在代码里写:
6、总结
掌握以下内容:
- CPU消耗型与IO消耗型
- 吞吐与响应的关系
- SCHED_FIFO 与SCHED_RR调度策略
- SCHED_NORMAL和CFS完全公平调度策略
- nice和renice
- chrt工具