SUSE Labs 团队探索了 Kernel CPU 隔离及其核心组件之一:Full Dynticks(或 Nohz Full),并撰写了本系列文章:
- CPU 隔离 – 简介
- CPU 隔离 – Full Dynticks 深探
- CPU 隔离 – Nohz_full
- CPU 隔离 – 管理和权衡
- CPU 隔离 – 实践
本文是第五篇。
我们通过前面四篇文章初步掌握了理论概念,现在终于到了实践阶段。本实践操作共配置了 8 个 CPU,将以完全隔离的方式在第 8 个 CPU 上运行一个无意义的用户空间循环,即:不受任何干扰。
内核配置要求
如果您运行的是 SUSE Linux Enterprise Server 15 SP3 (https://documentation.suse.com/sles/15-SP3/) 或更高版本,则无需担心这一问题;否则,一定要保证:
CONFIG_NO_HZ_FULL=y
CONFIG_CPUSETS=y
CONFIG_TRACING=y
第一条为在运行一个任务时停止 Tick 提供支持。第二条使任务绑定设置更容易。第三个选项支持对 CPU 隔离进行调试的跟踪能力。
引导要求
使用“nohz_full=” 引导参数,可以在运行单个任务时关闭计时器 Tick,并且大多数非内核负载也会迁移到隔离范围之外的 CPU。由于计划隔离第 8 个 CPU,我们需要通过以下信息引导内核:
nohz_full=7
CPU 编号从 0 开始,所以第 8 个 CPU 的编号为 7。此外,无需设置“rcu_nocbs=” 引导参数,如示例中通常显示的那样,nohz_full 可自动调节该参数。
任务绑定
有多种方法可以在隔离的任务和系统其余部分之间划分 CPU,首选方法是使用 cpuset 。对于有特殊需求的人,还有其他解决方案可用。
**Cpuset
**
一旦内核启动,为了确保无关任务不会干扰 CPU 7,我们创建两个 cpusets 分区。名为“isolation” 的目录包含我们隔离的 CPU,它将来会运行隔离任务。另一个名为“housekeeping” 的目录承担常规负载。我们强制禁用“isolation” 分区的负载平衡,以确保任何任务都不能迁移进/出 CPU 7,除非手工移动。
在本例中,我们在 SUSE Linux Enterprise Server 15 SP3 上使用默认的 cpuset 挂载点。
cd /sys/fs/cgroup/cpuset
mkdir housekeeping
mkdir isolated
echo 0-6 > housekeeping/cpuset.cpus
echo 0 > housekeeping/cpuset.mems
echo 7 > isolated/cpuset.cpus
echo 0 > isolated/cpuset.memse
cho 0 > cpuset.sched_load_balance
echo 0 > isolated/cpuset.sched_load_balance
while read P
do
echo $P > housekeeping/cgroup.procs
done < cgroup.procs
对 housekeeping/cgroup.procs 的一些写入操作可能会失败,因为内核线程 pid 无法移出根 cpuset 分区。然而,未绑定的内核线程会自动强制绑定 nohz_full 范围之外的 CPU,因此可以安全地忽略这些故障。
Isolcpus
您还可以使用“isolcpus=” (https://www.suse.com/support/kb/doc/?id=000017747) 内核引导参数实现与以上 cpuset 设置相同的设置。但我们不建议使用这种解决方案,因为以后无法在运行时更改隔离配置。因此,尽管“isolcpus” 仍在使用,但一般“不推荐”。对于尚未支持 cpusets/cgroups 的专用或嵌入式内核,它可能仍然可用。
Taskset, sched_setaffinity(), …
从底层来讲,还可以使用 taskset(https://man7.org/linux/man-pages/man1/taskset.1.html) 等工具或者依赖 sched_setaffinity() (https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html) 这样的 API,将每个任务绑定到所需的 CPU 集合。在不支持 cpuset 的系统中,其优点是允许在运行时更改绑定关系,这与“isolcpus” 不同;缺点是它需要更精细的工作。
IRQ 绑定
我们已经进行了任务绑定,但是硬件中断仍然可以在隔离的 CPU 上触发,并干扰其独占负载。所幸,我们可以通过 procfs (https://www.kernel.org/doc/html/latest/core-api/irq/irq-affinity.html) 提供的接口安排在内务管理集上触发这些中断:
Migrate irqs to CPU 0-6 (exclude CPU 7)
for I in $(ls /proc/irq)
do
if [[ -d "/proc/irq/$I" ]]
then
echo "Affining vector $I to CPUs 0-6"
echo 0-6 > /proc/irq/$I/smp_affinity_list
fi
done
您可能会在其中一个中断向量上遇到 I/O 错误,例如 x86-64 机器上的数字 0,因为这是每个 CPU 上的计时器向量表,由于其本地性的特质,无法将其移开。然而,这个问题大可放心忽略,因为“nohz_full” 就是专门为解决这个问题而设计的。
防止其他干扰
在本例中,我们处理的是基于调度程序和中断的直接干扰。更高阶的话题将在后续文章中介绍,例如防止页面错误等异常。
实际测试
现在,大部分内务管理工作负载应在 CPU 0-6 上运行。CPU 7 将在无干扰的情况下运行用户空间代码。我们用启动器做一个无意义的循环。
虚拟用户空间循环
以下代码将当前任务绑定到隔离的 cpuset(即 cpu7),并始终执行一个死循环。它在启动并运行 10 秒后,最终被单独的启动器启关闭。
include <stdio.h>
include <fcntl.h>
include <unistd.h>
include <errno.h>
int main(void)
{
// Move the current task to the isolated cgroup (bind to CPU 7)
int fd = open("/sys/fs/cgroup/cpuset/isolated/cgroup.procs", O_WRONLY);
if (fd < 0) {
perror("Can't open cpuset file...\n");
return 0;
}
write(fd, "0\n", 2);
close(fd);
// Run an endless dummy loop until the launcher kills us
while (1)
;
return 0;
}
将该代码包写在名为“user_loop.c” 的文件中,并编译:
$ gcc user_loop.c -o user_loop
启动器
除了在隔离的 CPU 7 上运行无意义的循环 10 秒之外,启动器的作用是跟踪可能对敏感工作负载有潜在干扰的事件。在本例中,我们使用 SUSE Linux Enterprise Server 15 SP3 上默认的跟踪 debugfs 装载点:
TRACING=/sys/kernel/debug/tracing/
Make sure tracing is off for now
echo 0 > $TRACING/tracing_on
Flush previous traces
echo > $TRACING/trace
Record disturbance from other tasks
echo 1 > $TRACING/events/sched/sched_switch/enable
Record disturbance from interrupts
echo 1 > $TRACING/events/irq_vectors/enable
Now we can start tracingecho 1 > $TRACING/tracing_on
Run the dummy user_loop for 10 seconds on CPU 7
./user_loop &
USER_LOOP_PID=$!
sleep 10
kill $USER_LOOP_PID
Disable tracing and save traces from CPU 7 in a file
echo 0 > $TRACING/tracing_on
cat $TRACING/per_cpu/cpu7/trace > trace.7
这里跟踪到两个有意思的底层事件:
调度程序上下文切换:报告任何抢占“user_loop” 的任务。这包括工作队列和内核线程。
IRQ 向量:报告任何(大多数)中断“user_loop” 的 IRQ,这包括计时器中断。
上述代码可以写入名为“launch” 的文件中,该文件与“user_loop” 位于相同的目录中。
完美世界的理想结果
在以 root 身份运行上述“launch” 之后,如果一切顺利,可以在“trace.7” 文件中找到以下内容:
-0 [007] d..2. 1980.976624: sched_switch: prev_comm=swapper/7 prev_pid=0
prev_prio=120 prev_state=R ==> next_comm=user_loop next_pid=1553
next_prio=120
user_loop-1553 [007] d.h.. 1990.946593: reschedule_entry: vector=253
user_loop-1553 [007] d.h.. 1990.946593: reschedule_exit: vector=253
在这里,user_loop 任务从时间戳 1980 秒处开始第一次跟踪,从 swapper(空闲任务)进行调度。然后,在 10 秒钟内(1990 – 1980),直到任务最终中断之前,任何事情都没有发生,这样做是为了处理发射器发送的关闭信号。这表明在这段时间内没有任务或中断干扰我们的 user_loop。
请注意:只有在完美世界中的完美机器上进行理想设置时,才会出现这种结果。