共计 2835 个字符,预计需要花费 8 分钟才能阅读完成。
这篇文章主要讲解了“Linux 高性能任务独占 CPU 举例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着丸趣 TV 小编的思路慢慢深入,一起来研究和学习“Linux 高性能任务独占 CPU 举例分析”吧!
Part 1 工程需求
在一个 SMP 或者 NUMA 系统中,CPU 的数量大于 1。在工程中,我们有时候有一种需求,就是让某个能够独占 CPU,这个 CPU 什么都不做,就只做指定的任务,从而获得低延迟、高实时的好处。
比如在 DPDK 中,通过设置
GRUB_CMDLINE_LINUX_DEFAULT=“isolcpus=0-3,5,7”
隔离 CPU0,3,5,7,让 DPDK 的任务在运行的时候,其他任务不会和 DPDK 的任务进行上下文切换,从而保证网络性能最佳 [1]。在 Realtime 应用场景中,通过 isolcpus= 2 隔离 CPU2,然后把实时应用通过 taskset 绑定到隔离的核:
taskset-c 2 pn_dev
从而保证低延迟要求 [2]。
Part 2 用户态隔离
这个地方,我们可以看出,它们统一都使用了 isolcpus 这样一个启动参数。
实践是检验真理的唯一标准,下面我们来启动一个 8 核的 ARM64 系统,运行 Ubuntu,并指定 isolcpus= 2 这个启动参数:
系统启动后,我们运行下面简单的程序 (启动 8 个进程运行 while 死循环):
我们是 8 核的,现在又是运行 8 个进程,所以理论上来讲,负载均衡后,8 个进程应该均分地运行在 8 个核上面,但是我们来看看实际的 htop 结果:
我们发现 3(也就是 CPU2) 上面的 CPU 占用率是 0.0%。这实证了 CPU2 已经被隔离,用户空间的进程不能在它上面跑。
当然,这个时候,我们可以通过 taskset,强行把其中的一个 a.out,绑定到 CPU2 上面去:
从上面命令的结果看出,663 原本的 affinity list 只有 0,1,3- 7 是没有 2 的,而我们强行把它设置为了 2,之后再看 htop,CPU2 上面占用 100%:
通过上面的实验,我们明显可以看出 isolcpus= 2 使得 CPU2 上无法再运行用户空间的进程了 (除非手动设置 affinity)。
Part 3 内核态隔离
中断
但是,能在 CPU2 上面运行的,不是只有用户态的任务,还可以有内核线程、中断等,那么 isolcpus= 能否隔离内核线程和中断呢?
对于中断,我们特别容易查看,就是实际去验证每个 IRQ 的 smp_affinity 就好了:
从上图明显可以看出,对于 44、47 号这种外设的中断,Linux 内核把 smp_affinity 设置为了 FB(11111011),明显避开了 CPU2,所以,实际外设中断也不会在 CPU2 发生,除非我们强行给中断绑核,比如让 44 号中断绑定到 CPU2:
echo 2 /proc/irq/44/smp_affinity_list
之后,我们发现 44 号中断在 CPU2 可以发生:
但是,系统的 timer 中断、IPI,由于是 Linux 系统的运行基石,实际还是要在 CPU2 上面运行的。这里面最可能给任务带来延迟抖动的,自然是 timer tick。
下面我们重点探讨下 tick 的问题,由于 Linux 一般情况下,已经配置 IDLE 状态的 NO_HZ tickless,所以 CPU2 上面什么都不跑的时候,实际 timer 中断几乎不发生。
下面,我们还是在 isolcpus= 2 的情况下,运行前面那个 8 个进程的 a.out,默认情况下没有任务会占用 CPU2。通过先后运行几次 cat /proc/interrupts | head 2,我们会看到其他 core 的 timer 中断频繁发生,而 CPU2 几乎不变,这显然是 IDLE 时候的 NO_HZ 在发挥省电的作用:
但是,一旦我们放任务到 CPU2,哪怕只是放 1 个,就会发现 CPU2 上面的 timer 中断开始增加:
这说明一点,哪怕隔离的 CPU 上面只有一个线程去跑,timer tick 就会开始跑,当然,这个 timer tick 也会频繁打断这一个线程,从而造成大量的上下文切换。你肯定会觉得 Linux 怎么这么傻,既然只有一个人,那也没有时间片分片的必要,不需要在 2 个或者多个任务进行时间片划分地调度,为啥还要跑 tick? 其实原因是我们的内核默认只是使能了 IDLE 的 NO_HZ:
我们来重新编译一个内核,使能 NO_HZ_FULL:
当我们使能了 NO_HZ_FULL 后,Linux 支持在 CPU 上仅有 1 个任务的时候,是可以 NO_HZ 的。但是有 2 个就傻眼了,所以这个“FULL”也不是真地 FULL[3]。这当然也可以理解,因为有 2 个就涉及到时间片调度的问题。什么时候应该使能 NO_HZ_FULL,内核文档 Documentation/timers/no_hz.rst 有明确地“指示”,只有在实时和 HPC 等的场景,才需要,否则默认的 NO_HZ_IDLE 是你最好的选择:
我们重新编译了内核,选中了 NO_HZ_FULL,下面启动 Linux,注意启动的时候参数添加 nohz_full=2,让 CPU2 支持 NO_HZ_FULL:
重新运行 CPU2 只有一个任务的场景,看看它的 timer 中断发生情况:
发现 CPU2 上面的 tick 稳定在 188 上面,这样相信你会更加开心,因为你独占地更加彻底了!
下面,我们再放一个 task 进去 CPU2,有 2 个任务的情况下,CPU2 上面的 timer tick 开始增加:
不过,这或许不是个问题,因为我们说好了“独占”,1 个任务独占的时候,timer tick 不来打扰,应该已经是非常理想的情况了!
内核态线程
内核态的线程其实和用户态差不多,当它们没有绑定到隔离的 CPU 的时候,是不会跑到隔离 CPU 运行的。下面用笔者在内核里面添加的 dma_map_benchmark 来做实验 [4],开启 16 个内核线程来进行 DMA map 和 unmap(注意我们只有 8 个核):
./dma_map_benchmark -s 120 -t 16
我们看到 CPU2 上面的 CPU 占用也是 0:
内核里面的 dma_map_benchmark 线程在狂占 CPU0-1, 3-7,但是就是不去占 CPU2:
但是,内核线程如果用 kthread_bind_mask() 类似 API 把线程绑定到了隔离的 CPU,则情况就不一样了,这就类似用 taskset 把用户态的任务绑定到 CPU 一样。
Part 4 最佳实践指南
对于实时性要求高、高性能计算等场景,如果要让某个任务独占 CPU,最理想的选择是:
1. 采用 isolcpus 隔离 CPU
2. 将指定任务绑定到隔离 CPU
3. 小心意外地把中断、内核线程绑定到了隔离 CPU,排查到这些“意外”分子
4. 使能 NO_HZ_FULL,则效果更佳,因为连 timer tick 中断也不打扰你了。
感谢各位的阅读,以上就是“Linux 高性能任务独占 CPU 举例分析”的内容了,经过本文的学习后,相信大家对 Linux 高性能任务独占 CPU 举例分析这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是丸趣 TV,丸趣 TV 小编将为大家推送更多相关知识点的文章,欢迎关注!