Linux进程(七):调度
所有的操作系统,在设计调度器的时候,都是朝着两个目标去的:1、缩小响应时间:最小化某个任务的响应时间,哪怕牺牲其他的任务为代价。2、提高吞吐率:使整个系统的
workload
被最大化处理。但是这两个目标往往不可兼得,提高响应可能将会降低吞吐率,而提高吞吐率响应时间又将变长。因为吞吐率提高表示着资源都花费在了做有用功上,而缩小响应时间所产生的CPU抢占势必将减少吞吐率(此处不仅仅是上下文切换的时间,还包括CPU的
cache miss
之后所发生=的一系列处理)。
调度模型
在Linux内核的编译选项中,我们可以选择操作系统的进程调度模型(Preemption Model),在Linux中一般分为以下三种:
-
No Forced Preemption (Server)
-
Voluntary Kernel Preemption (Desktop)
-
Preemption Kernel (Low-Latency Desktop)
这三种选择将影响到Linux中的调度算法。第一种表示不强制抢占、第二种表示不允许内核抢占、第三种表示允许内核抢占。这三种调度模型中抢占强度由低到高,这意味着从相应速度来看这三种模型也是从低到高,同时吞吐率则为由高到低。因此在一些需要高吞吐率的环境中我们一般使用第一种抢占模型(例如服务器),而在一些需要高相应速度的系统中我们一般使用第三种抢占模型(例如手机或桌面电脑)。
I/O消耗型 vs. CPU消耗型
在操作系统中的进程可分为I/O消耗型和CPU消耗型:
-
I/O消耗型的进程CPU利用率低,进程的运行效率主要受限于I/O速度。
-
CPU消耗型的进程多数时间花在CPU上做运算。
I/O消耗型的任务主要关注与系统是否能够及时把该任务调度到,能够及时拿到CPU。所以在一个典型的计算机操作系统中,一般I/O消耗型任务都会比CPU消耗型任务优先调度。并且I/O消耗型任务通常都伴随着用户体验(比如说鼠标输入就是I/O消耗型任务,一直等待鼠标的输入,鼠标一有I/O输入必须立即调度并执行响应,不然用户将会感觉到卡顿)。
所以一般I/O消耗型任务对于拿到的CPU性能好坏并不是特别敏感,而对于是否能及时获取到CPU极为敏感。在此基础上,ARM研发了一套big.LITTLE CPU架构:几个负责运算的高功耗CPU专门负责CPU消耗型任务的处理,而几个低功耗的CPU则只负责I/O消耗型任务的处理。同时在系统的进程调度中,将I/O消耗型的任务统一调度到低功耗CPU上,这样可以通过这种CPU架构利用小功耗达到高性能效果。
调度器实现
早期2.6:优先级数组和bitmaps
早期的Linux2.6的调度器在Linux内核空间中把整个Linux的优先级划分为0-139,并且在内核空间中数值越小,优先级越高(但是在用户空间设置进程的优先级的时候时反着设置的,例如我们在用户空间设置进程的优先级为50,在Linux内核中会把99-40最终得到的59设置为该进程在内核空间中的优先级,所以在用户空间中设置的优先级数值越大优先级越高)。注意,100-139这个优先级的进程不会抢占CPU,只有走到0-99优先级的进程无进程可调度之后才会调度100-139优先级(也就是用户空间-20~19优先级)的进程。在每一次调度的时候内核从第0位开始查看哪一位上有进程可被调度,若检测到可被调度的进程则将其置为运行态。
RT调度策略:
SCHED_FIFO
:不同优先级按照优先级高的先跑直到睡眠,优先级低的在跑;同等优先级先进先出。SCHED_RR
:不同优先级按照优先级高的先跑直到睡眠,优先级低的再跑,同等优先级轮转(RR - RoundRobin)。
普通进程(100 - 139优先级的进程)将在不同优先级直接进行轮转,但是不会等到sleep
或wait
之后再释放CPU,而是CPU会一直在这些进程中轮转。此时优先级高的优势在于:1、可以得到更多的时间片。2、当从睡眠中被唤醒时可以抢占优先级低的进程,然后再进行轮转。
此处普通进程的nice
值(-20~19)是会被操作系统不停更改的:一个进程睡眠次数越多,nice
值将越低,优先级越高。而睡眠次数少的进程nice
值将变高,优先级降低。这是为了时I/O消耗型任务能够优先获取到CPU而提高响应速度。
为了防止RT任务(0-99优先级)一直占用CPU而普通任务得不到执行的机会,Linux后期加入了/proc/sys/kernel/sched_rt_period_us
和/proc/sys/kernel/sched_rt_runtime_us
,限制了在一个时间周期内RT进程所占用的时间比例。
schedule normal调度算法
CFS:完全公平调度
完全公平调度算法利用了一颗红黑树,节点中存放的值为进程运行到目前为止的vruntime
。每一次Linux调度都调度当前vruntime
最小的task_struct
。其中vruntime = runtime * (NICE_0_LOAD / nice_n_weight)
。而nice_n_weight
的值由nice
值决定,如下所示:
在这个表中我们可以找到规律:高一个等级的weight
是第一个等级的1.25倍。
当进程被调度到时,进程将得到CPU的运行时间,则进程的物理运行时间的值将逐渐增大,当增大到某个程度时,该进程的vruntime
在红黑树中的值将不是最小,此时操作系统更新红黑树之后再去调度红黑树中的最左边节点的进程,实现了完全公平调度算法。
CFS调度算法引入的vruntime
不仅记录了每个进程的权重信息,还包含了每个进程的CPU占用时间。这种做法完美解决了I/O消耗型任务和CPU消耗型任务调度的优先级问题,因为I/O消耗型任务的CPU占用时间注定是比CPU消耗型任务的运行时间少,因此vruntime
的分子也将变小。
CFS调度算法致力于每一个进程的virtual runtime
时间相等,那就意味着若两个进程的weight
若相差了三倍,则真实的CPU利用率也将相差三倍。
例
以下通过实验证明以上观点,以下一个程序将创建八个线程并执行死循环:
#define N 8
void *thread_fun(void *param)
{
printf("thread pid:%d, tid:%lu\n", getpid(), pthread_self());
while (1) ;
return NULL;
}
int main(void)
{
pthread_t tid[N];
int ret;
printf("main pid:%d, tid:%lu\n", getpid(), pthread_self());
for (int i = 0; i < N; i++){
ret = pthread_create(&tid[i], NULL, thread_fun, NULL);
if (ret == -1){
perror("cannot create new thread");
return 1;
}
}
for (int i = 0; i < N; i++){
if (pthread_join(tid[i], NULL) != 0){
perror("call pthread_join function fail");
return 1;
}
}
return 0;
}
在默认情况下,该进程中所有线程的nice值都相同,我们可以启动两次上述程序,若不做任何干涉,两个进程的CPU占用率应该相同:
第一个进程:
第二个进程:
此时我们使用top
命令观察27618和27627这两个进程的CPU使用情况,都是接近200%(两个线程每个线程占用一个核):
若此时我们使用命令renice -n -5 -g 27618
将27618的nice
值设置为-5(-5的权重相比于0的权重来说,-5的权重比0的权重大了将近3倍),由于CFS需要保持vruntime
的一致性,所以27618的runtime
必须为27627的三倍,也就是说27618的CPU利用率将会是27627的三倍:
在改变完27618进程的nice
值之后,我们可以看到top
命令输出的参数已经发生变化:
注意:该效果需要在CPU满负荷情况下才能实现,若CPU存在空闲,则进程始终将会被调度执行。
调度相关的系统调用
在C/C++当中,可以去调用以下api去做进程调度的相关选项:
System Call | Description |
---|---|
nice() | Sets a process’s nice value |
sched_setscheduler() | Sets a process’s scheduling policy |
sched_getscheduler() | Gets a process’s scheduling policy |
sched_setparam() | Sets a process’s real-time priority |
sched_getparam() | Gets a process’s real-time priority |
sched_get_priority_max() | Gets the maximum real-time priority |
sched_get_priority_min() | Gets the minimum real-time priority |
sched_rr_get__interval() | Gets a process’s timeslice value |
sched_setaffinity() | Sets a process’s processor affinity |
sched_getaffinity() | Gets a process’s processor affinity |
sched_yield() | Temporarily yields the processor |