概述
理想状态下,当系统中有多个程序在运行时,我们希望他们是同时运行的,共享cpu时间,按需分配cpu算力。而实际情况是:一个cpu同一时间只能运行一个程序,那想要模拟理想状态,就只能每个程序运行一小段时间,轮流运行,只要切换足够快,用户无法感知,也就实现了用户视角下的并行。
但任务对用户来说并不一视同仁,有轻重缓急之分。用户希望一些关键的程序可以有很好的响应度,而用户不关心的程序可以运行较短时间。我们引入进程优先级的概念来解决这个问题。
我们为普通进程设立-20~19共40个优先级(此外还有实时、deadline、idle进程,我们这里不讨论),数值越小,优先级越高,也就应该分到更多的cpu时间。如何为对应的优先级分配cpu时间呢?
时间片
先尝试给每个优先级对应的进程分配固定时间的时间片,看看效果如何?我们给优先级最小的19分配5ms时间片,优先级每增加1,则多5ms时间。
优先级和时间片对应关系如下:
优先级 | 时间片 |
---|---|
19 | 5 |
18 | 10 |
17 | 15 |
… | … |
-18 | 190 |
-19 | 195 |
-20 | 200 |
当系统中有优先级分别为19和18的两个进程A和B时,A运行5ms后换B运行10ms,以此往复。这样B会比A多整整一倍的运行时间;而如果A的优先级时-19,B的优先级是-20,同样优先级只高了一级,但B只比A多了2.5%的时间!这相差也太大了。
除此之外,还有一个问题是如果有10个优先级为-20的进程,那每个进程运行完要等1800ms才能在运行,如果进程更多,那肯定是无法忍受的了。
通过将固定时间片调整为相对时间,可以解决第一个问题,即优先级差值和时间片的差值成比例增长。但仍无法解决第二个问题。
调度周期
可以引入调度周期来解决调度时间太久的问题。
我们可以设定一个系统和用户可以接受的调度周期,在Linux kernel中这个值是6ms(仅当进程数小于8时),如果进程超过8个,则调度周期是0.75ms * 进程数
/*
* The idea is to set a period in which each task runs once.
*
* When there are too many tasks (sched_nr_latency) we have to stretch
* this period because otherwise the slices get too small.
*
* p = (nr <= nl) ? l : l*nr/nl
*/
static u64 __sched_period(unsigned long nr_running)
{
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
else
return sysctl_sched_latency;
}
unsigned int sysctl_sched_latency = 6000000ULL;
unsigned int sysctl_sched_min_granularity = 750000ULL;
static unsigned int sched_nr_latency = 8;
更好的方案
现在我们知道了想要合理分配进程的cpu资源,需要考虑两个方面:
- 不能使用固定时间片,这样会导致相同差异的优先级分配到的资源比例不一样
- 需要根据进程数量来选用合适的调度周期,太长会导致巨大的延迟,太短会导致频繁调度,没有足够的时间留给程序运行
上一节已经讲了kernel对于调度周期的处理:进程数少于8个时固定为6ms,否则等于进程数*0.75ms;那时间片的问题呢?
既然不能有固定时间片,那干脆就不要时间片了,转为分割cpu时间比例。例如如果是两个优先级相等的进程,那么他们各占50%的cpu时间,调度周期为6ms的话就是各占3ms。设定优先级每升一级,可多获得10%的cpu时间。这样,我们就可以根据设定的调度周期和当前进程数来为各个进程分配合理的cpu时间。
Linux kernel中的实现
kernel中设定进程运行时间的函数是sched_slice
/*
* We calculate the wall-time slice from the period by taking a part
* proportional to the weight.
*
* s = p*P[w/rw]
*/
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
unsigned int nr_running = cfs_rq->nr_running;
u64 slice;
if (sched_feat(ALT_PERIOD))
nr_running = rq_of(cfs_rq)->cfs.h_nr_running;
/*
*根据当前task数量来设定调度周期
*/
slice = __sched_period(nr_running + !se->on_rq);
for_each_sched_entity(se) {
struct load_weight *load;
struct load_weight lw;
/*
*当前调度实体se所在的运行队列cfs_rq和该队列的总负载laod
*/
cfs_rq = cfs_rq_of(se);
load = &cfs_rq->load;
if (unlikely(!se->on_rq)) {
lw = cfs_rq->load;
update_load_add(&lw, se->load.weight);
load = &lw;
}
/*
*简化一下就是slice * se->load.weight / laod
*/
slice = __calc_delta(slice, se->load.weight, load);
}
if (sched_feat(BASE_SLICE))
slice = max(slice, (u64)sysctl_sched_min_granularity);
return slice;
}
weight值是和nice值一一对应的,代表不同优先级的进程在系统中的权重,其对应关系为:
const int sched_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,
};
例如现在系统中同时运行nice值为0的task A和nice值为1的task B
A.weight = 1024
B.weight = 820
现调度周期为6ms
那A可获得的时间为:6 * 1024 / (1024+820) ~= 3.33
B可获得的时间为:6 * 820 / (1024+820) ~= 2.67
这些值的设计理念是当nice值降低1时,会获得比原来多10%的cpu时间; nice+1时会比原来多出10%的cpu时间。相邻nice值之间的差值约为25%。当nice值为0时,weight为1024。以这个值为标准,nice值为1时,weight = 1024*1.25=1280,nice值为-1时,weight = 1024/1.25~=819。以此类推,可以计算出整个表的值。
注意:
大家可以看到,实际sched_prio_to_weight中的值和我们计算结果有出入,其实刚开始确实是和我们计算的值一样,只是后面为了使weight*inv_weight更精确,略微调整了每个值。
相关patch:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=254753dc321ea2b753ca9bc58ac329557a20efac
可参考这个邮件讨论:https://lkml.org/lkml/2019/10/7/1117