3.5 本章总结与附录

查看的输出结果是一个掩码。2用二进制表示的话是 0010,倒数第二位为1,表示亲和到了第2个核(0开头为第一个,第2个核的序号是1)。

taskset工作原理是调用内核提供的 sched_setaffinitysched_getaffinity 两个函数来实现查看和修改进程的CPU亲和性。你在程序中也可以直接调用这两个函数来完成同样的工作。其中设置调用的是 sched_setaffinity,通过一系列的调用,最后在 set_cpus_allowed_common 将用户输入的核号信息设置到进程的 task_struct 上。

// file:kernel/sched/core.c
void set_cpus_allowed_common(struct task_struct *p, const struct cpumask *new_mask)
{
	cpumask_copy(&p->cpus_mask, new_mask);
	p->nr_cpus_allowed = cpumask_weight(new_mask);
}

可见,CPU亲和信息被写在 p->cpus_mask。另外进程上的 p->cpus_ptr 默认是指向 p->cpus_mask 的。这样完全公平调度器的负载均衡模块在工作的时候,通过判断进程上的 cpus_ptr 变量,就不会胡乱地迁移我们的进程了。

本章总结

好了,我们把本章内容总结一下。本章从以下几个下面几个方面展开对调度器的介绍。

1)调度器的发展简史

Linux的调度器发展大概是经历了 O(n) 调度器、O(1) 调度器和完全公平调度器几个过程。在 O(n) 调度器中,整个系统只有一个任务队列,锁竞争严重。在 O(1) 调度器中为每个逻辑核都设置了一个任务队列,解决了锁竞争的问题。每个进程的时间片是按优先级来提前算出来的,优先级越高,运行时间越长。这会导致在任务比较多的时候,调度延迟非常的不可控。完全公平调度器 CFS 通过引入 vruntimenice 等概念对原来的优先级和时间片都进行了重构。首先根据当前系统的情况动态地计算调度周期。然后根据调度周期和每个进程的 vruntime 来动态地计算是否该调度下一个进程了。整体上维护的是运行时间的相对公平。

2)调度器是如何定义的,运行队列到底长什么样子?

在本书使用的 5.4 版本内核中,Linux 调度器会为每一个核都准备一个运行队列,struct rq。在每个运行队列中,分为实时进程和普通进程两种。

实时进程的任务相比普通进程有绝对高的优先权。实时进程在实现上是通过多优先级任务队列的方式实现的。优先级分别为 0-99。每个优先级都有一个用链表实现的队列。优先级高的会抢占低优先级的任务的 CPU。同优先级内部通过时间片轮转或者先来先服务的算法来调度。

在普通进程中不再使用链表,而是使用了一棵以 vruntime 大小为 key 的红黑树。调度器在选择下一个进程的时候,直接从红黑树最左侧的节点选择即可,因为它是整个用户进程任务队列中 vruntime 最小的任务,也是对 CPU 需求最迫切的任务。红黑树中的每个节点都是一个调度实体,这个调度实体一般来说就是一个进程,但也有可能是一个进程组。

3)进程是如何确定自己该加入哪个运行队列的?

系统中往往会存在很多个核,每个核都有一个任务队列。所以进程在刚创建的时候和被唤醒的时候需要来确定加入哪个运行队列。在选择上,会尽量优先选择要唤醒的进程上一次使用的 CPU 逻辑核,或者是正要唤醒它的进程所在的核(当前逻辑核)。因为这两个核上大概率缓存还是热的,进程调度上来会运行的比较快。然后是考虑共享 cache 且 idle 的 CPU,然后就是考虑负载最小的核。在综合考虑缓存友好性以及空闲状况后,选择一个 CPU 运行队列出来,并将新进程添加到该队列的红黑树中。

4)调度器是何时触发选择下一个待运行进程的?

调度器主要在两个时机中会触发真正的任务的调度。一是调度节拍,二是其他任务主动放弃 CPU 的时候。其中调度节拍是调度器中非常重要的执行入口。在每个核上都会定时触发调度节拍的执行。在这里会计算当前核上所有任务的 vruntime,并判断是否需要切换下一个任务上来运行。然后还有会触发负载均衡,判断其它核是否过忙了。如果是的话,就主动去迁移一些任务到当前核上来执行。

通过以上四步,我们就对 Linux 的调度器有了更深入的理解和认识了。接着我们再来看开篇中提到的几个实践中息息相关的问题。

1)进程不主动释放 cpu 的话,每次调度最少能运行多久?

首先要明白的是过于频繁的进程上下文切换对系统的性能是非常有害的。这就好比每一分钟都切换大脑去处理一个其它的问题,你的工作效率根本就提不起来。在人的工作上,我们有个高效的方法叫番茄工作法,目的是保证你在一段时间内,比如20分钟左右,只专心致志地处理一个工作。下一个工作在下个番茄时钟到来时再处理。进程调度也是如此。在完全公平调度器中,出于减少频繁切换进程所带来的成本考虑,一个进程一旦被分配到 CPU 就会持续运行相对较长的一段时间,避免频繁的进程上下文切换导致的性能损耗。这段时间的最小值由 sched_min_granularity_ns 这个内核参数来控制,单位是 ns (纳秒)。例如下面这个配置的最短运行时间是 10 ms。

# sysctl -a | grep min_granularity
kernel.sched_min_granularity_ns = 10000000

当然了,如果进程因为等待网络、磁盘等资源时主动放弃 CPU 那另算。

2)现在的进程调度还是按时间片来执行的吗?

根据历史的经验,我们经常喜欢说进程运行了多长的时间片。但其实现在的完全公平调度器中早已不再是按时间片来调度进程了。每次调度节拍中是根据当前在运行的任务的 vruntime 和任务队列中最小的 vruntime 来比对,判断是否有必要把下一个进程给切换上来。

3)进程的 nice 值的含义是什么?

很多人都喜欢把 nice 值说成是优先级。我认为这是不恰当的。对实时进程来说优先级是在1-99之间。但对于使用 SCHED_OTHER 策略的普通用户进程来讲,优先级全部都是0。优先级讲究的是抢占。优先级高的拥有绝对的调度优先权。但普通进程的 nice 的含义其实是一个对 CPU 分配上的一个权重。对于 nice 值低(会抢占更多CPU资源)的进程,内核并不一定绝对优先调度它,而仅仅只是保证它在整个运行的过程中获得的 CPU 比例会多一些。反之,如果 nice 值高(会让着其它进程),内核会给它分配的 CPU 比例就会低一些。但如果 nice 高的进程等待的时间过长的话,仍然可能会被优先调度。总之,nice 代表的是一个权重比例,而不是优先级。

4)用户进程中,高优先级是否能抢占低优先级的 CPU?

在实时任务如 migration 内核线程中,是按优先级调度的。优先级强调的是抢占,高优先级比低优先级有优先获得 CPU 的权利。但是对于用户进程来讲,一般都采用的完全公平调度器来进行 CPU 资源的分配。在这种调度器中,其 nice 其实是一个权重的概念,而不太像传统的优先级。优先级强调的是抢占,高优先级比低优先级有优先获得 CPU 的权利。而用户进程中的 nice 值强调的是获取到 CPU 运行时间的权重比例。在完全公平调度器中,真正决定是否能抢占的只有 vruntime,而不是 nice 的高低。只不过 nice 越低的进程,其 vruntime 下降的越快,触发抢占的可能性越高。

5)业界流行的在线离线混部有啥副作用没?

现在业界的公司为了更充分地将 CPU 利用率给拉起来,降低成本,会在在线业务的机器上部署离线计算任务。但是这里面有一个被很多人都忽视的细节,那就是在线离线混布会破坏掉内核的 wake_affine 机制。内核调度时为了性能考量,会倾向于让 CPU 在之前自己跑过的核上运行,这样 CPU 的缓存还有不少能用的。但是在离线混部会让 wake_affine 机制成功的概率大大降低。进程在不同的核上运行概率增加,Cache 中的数据都是凉的,穿透到内存的访问次数增加,进程的运行性能就会下降很多。

6)为什么进程执行会在CPU各个核之间飘来飘去?

# chrt --max
SCHED_OTHER min/max priority  : 0/0
SCHED_FIFO min/max priority : 1/99
SCHED_RR min/max priority : 1/99

既然系统中存在多个任务队列,就有可能会出现有的任务队列特别忙,有的特别闲。所以完全公平调度器还有个负载均衡模块,在调度节拍触发的时候会主动去其它队列上看看能不能帮忙。这个机制中有可能会将其他核的任务队列中的进程拉取到自己核上来执行。因为负载均衡机制的存在,所以进程可能会在各个核之间飘来飘去,而不是只固定使用某一个核。

7)taskset命令是如何让一个进程钉在某个核上的?

我去理发店理发的时候,不管有多少个理发师,我只会选择我最熟悉的那一位。在进程调度中也是类似,如果你不想让负载均衡模块把你的进程拉到别的核上去运行。你可以使用 taskset 命令来设置你进程的CPU亲和性。在设置完后,负载均衡模块会判断它,会尊重你的亲和性设置。这样你的进程就可以只在你希望的核上运行了。

最后

最后,我想说的是 Linux 在调度器上固然已经做了很多事情了。它关注的重点就是咱们前面说到过的某个CPU核要运行哪个进程,以及运行多长时间。但对于我们开发者来说,我们也许更应该关注的是进程调度到哪个CPU核上运行。因为对核的亲和性做的好,会对应用程序有较大的性能帮助。因此在完全公平调度器基础上合理地使用 taskset、或者是 cgroup 下的 cpuset 等让进程按照我们预期的核来调度,也许是我们更应该关注的事情。

在知识星球中我们通过视频讲解,让你的底层学起来更快,事半功倍。对大家技术深度和广度的积累很有好处。通过下方二维码加入知识星球目前只需要299一年。到期前续费的话,目前设置的是6折,也是299一年,后期随着课程的更新,优惠会逐步取消。