如何保护CPU免受Linux调度程序的影响(防止将线程调度到该CPU上)?

44

可以使用sched_setaffinity将线程固定到一个CPU上,这样可以提高性能(在某些情况下)

来自Linux man页:

将进程限制为在单个CPU上运行也可以避免缓存失效引起的性能损失,当进程停止在一个CPU上执行并重新开始在另一个CPU上执行时会发生。

此外,如果我需要更实时的响应,可以将该线程的调度器策略更改为SCHED_FIFO,并将优先级提高到某个高值(最大可达sched_get_priority_max),这意味着该线程在就绪时应始终优先于在其CPU上运行任何其他线程。

然而,在这一点上,正在运行在实时线程刚刚抢占的CPU上的线程可能已经清除了大部分实时线程的L1缓存条目。

我的问题如下:

  1. 是否可以防止调度程序将任何线程调度到特定的CPU上?(例如:完全从调度程序中隐藏CPU或以其他方式)
  2. 是否有一些必须能够在那个CPU上运行的线程?(例如:内核线程/中断线程)
  3. 如果我需要在那个CPU上运行内核线程,使用什么合理的最大优先级值以使我不会让内核线程饿死?

你好,我也遇到了同样的问题。你有发现任何新的信息吗? - Hong Zhou
@HongZhou - 我找到了cpuset实用程序,它正好符合我的要求-请查看我添加的答案。 - Steve Lorimer
4个回答

58
答案是:使用cpusetspython cpuset utility使其易于配置。

基本概念

3个cpusets

  • root:包含所有CPU(未屏蔽的),在所有配置中都存在
  • system:包含用于系统任务的CPU - 那些需要运行但不是“重要”的任务(未屏蔽的
  • user:包含用于“重要”任务的CPU - 那些我们想以“实时”模式运行的任务(屏蔽的

shield命令管理这3个cpusets。

在安装期间,它将所有可移动的任务移动到未屏蔽的cpuset(system),并在拆卸期间将所有可移动的任务移动到root cpuset 中。 安装完成后,子命令允许您将任务移动到shielduser)cpuset中,并且还可以将特殊任务(内核线程)从root移动到system(因此从user cpuset中移除)。

命令:

首先,我们创建一个屏蔽。自然情况下,屏蔽的布局将取决于机器/任务。例如,假设我们有一个4核非NUMA机器:我们要将 3个内核分配给屏蔽,并留下 1个内核用于不重要的任务;由于它是非NUMA,因此我们不需要指定任何内存节点参数,并且在root cpuset(即跨所有CPU)中运行内核线程。

$ cset shield --cpu 1-3

一些内核线程(那些没有绑定到特定CPU的线程)可以被移动到system cpuset中。(通常情况下,将已绑定到特定CPU的内核线程移动是不明智的。)

$ cset shield --kthread on

现在让我们列出在受保护的(user)或未受保护的(system) cpuset中正在运行的内容: (使用-v可以列出进程名称)(添加第二个-v以显示超过80个字符)

$ cset shield --shield -v
$ cset shield --unshield -v -v
如果我们想要停止屏蔽(拆卸)
$ cset shield --reset

现在让我们在保护模式下执行一个进程(在 '--' 后面的命令将传递给要执行的命令,而不是 cset

$ cset shield --exec mycommand -- -arg1 -arg2

如果我们已经有一个正在运行的进程,想要将其移动到盾牌中(注意,我们可以通过传递逗号分隔的列表或范围来移动多个进程(范围内的任何进程都将被移动,即使存在间隔))

$ cset shield --shield --pid 1234
$ cset shield --shield --pid 1234,1236
$ cset shield --shield --pid 1234,1237,1238-1240

高级概念

cset set/proc - 这些命令可以更精细地控制cpusets

集合

创建、调整、重命名、移动和销毁cpusets

命令

使用cpu 1-3,使用NUMA节点1创建一个名为"my_cpuset1"的cpuset

$ cset set --cpu=1-3 --mem=1 --set=my_cpuset1

将"my_cpuset1"更改为仅使用CPU 1和3

$ cset set --cpu=1,3 --mem=1 --set=my_cpuset1

销毁一个cpuset

$ cset set --destroy --set=my_cpuset1

重命名现有的cpuset

$ cset set --set=my_cpuset1 --newname=your_cpuset1
创建一个分层的cpuset。
$ cset set --cpu=3 --mem=1 --set=my_cpuset1/my_subset1
  • 列出现有的cpuset(第1级深度)
$ cset set --list
列出当前cpuset及其子节点。
$ cset set --list --set=my_cpuset1
列出所有现有的cpuset。
$ cset set --list --recurse

Proc

管理线程和进程

命令

列出在cpuset中运行的任务

$ cset proc --list --set=my_cpuset1 --verbose

在cpuset中执行任务

$ cset proc --set=my_cpuset1 --exec myApp -- --arg1 --arg2

移动任务

$ cset proc --toset=my_cpuset1 --move --pid 1234
$ cset proc --toset=my_cpuset1 --move --pid 1234,1236
$ cset proc --toset=my_cpuset1 --move --pid 1238-1340

移动一个任务及其所有兄弟任务

$ cset proc --move --toset=my_cpuset1 --pid 1234 --threads

将所有任务从一个cpuset移动到另一个cpuset

$ cset proc --move --fromset=my_cpuset1 --toset=system

将未固定的内核线程移动到一个cpuset中

$ cset proc --kthread --fromset=root --toset=system

将内核线程(包括那些被固定到特定CPU上的线程)强制转移到一个cpuset中(注意:这可能会对系统产生严重后果 - 确保您知道您在做什么)

$ cset proc --kthread --fromset=root --toset=system --force

层级示例

我们可以使用分层cpusets创建优先级分组。

  1. 创建具有1个cpu(0)的system cpuset。
  2. 创建具有1个cpu(1)的prio_low cpuset。
  3. 创建具有2个cpu(1-2)的prio_met cpuset。
  4. 创建具有3个cpu(1-3)的prio_high cpuset。
  5. 创建具有所有4个cpu(0-3)的prio_all cpuset(请注意,这与root相同;将其与root分离是一个良好的实践)。

为了实现上述目标,您需要创建prio_all,然后在其中创建子集prio_high等。

$ cset set --cpu=0 --set=system
$ cset set --cpu=0-3 --set=prio_all
$ cset set --cpu=1-3 --set=/prio_all/prio_high
$ cset set --cpu=1-2 --set=/prio_all/prio_high/prio_med
$ cset set --cpu=1 --set=/prio_all/prio_high/prio_med/prio_low

1
这个答案太棒了。谢谢! - Søren Løvborg

8

我能想到另外两种方法来做这件事(虽然不如cset那么优雅,而且似乎在Redhat上的支持水平也不是很好):

1)将所有进程包括PID 1都绑定到一个或多个“垃圾核心”上 - 简单易行(但是据说会导致调度程序效率低下,尽管我自己从未遇到过任何问题)。下面的脚本(必须以root身份运行)对所有正在运行的进程(包括init(pid 1))运行taskset;这将把所有正在运行的进程固定在一个或多个“垃圾核心”上,并通过将init固定在其中,确保任何未来的进程也在“垃圾核心”列表中启动:

#!/bin/bash

if [[ -z $1 ]]; then
  printf "Usage: %s '<csv list of cores to set as junk in double quotes>'", $0
  exit -1;
fi

for i in `ps -eLfad |awk '{ print $4 } '|grep -v PID | xargs echo `; do 
   taskset -pc $1 $i;
done

2)使用isolcpus内核参数(这里是来自https://www.kernel.org/doc/Documentation/kernel-parameters.txt的文档):

isolcpus=   [KNL,SMP] Isolate CPUs from the general scheduler.
            Format:
            <cpu number>,...,<cpu number>
            or
            <cpu number>-<cpu number>
            (must be a positive range in ascending order)
            or a mixture
            <cpu number>,...,<cpu number>-<cpu number>

        This option can be used to specify one or more CPUs
        to isolate from the general SMP balancing and scheduling
        algorithms. You can move a process onto or off an
        "isolated" CPU via the CPU affinity syscalls or cpuset.
        <cpu number> begins at 0 and the maximum value is
        "number of CPUs in system - 1".

        This option is the preferred way to isolate CPUs. The
        alternative -- manually setting the CPU mask of all
        tasks in the system -- can cause problems and
        suboptimal load balancer performance.

我曾在几个项目中使用这两种方法和cset机制(顺便说一下,请原谅我明目张胆的自我推销 :-))。我刚刚为一个名为Pontus Vision ThreadManager的工具申请了专利,它可以为任何给定的x86平台和软件工作负载提供最佳固定策略;在客户现场测试后,我得到了非常好的结果(峰值延迟降低了270%),因此进行固定和CPU隔离是非常值得的。

2
这里介绍一种使用cgroups的老式方法来实现。我有一台运行Fedora 28的机器,RedHat/Fedora建议使用systemd-run,但我在其中未能找到这个功能。如果有人能告诉我如何使用systemd-run来实现这个功能,我会非常感激。
假设我想要将第四个CPU(0-3)从调度中排除,并将所有现有进程移动到CPU 0-2。然后,我想将一个进程单独放在CPU 3上。
sudo su -
cgcreate -g cpuset:not_cpu_3
echo 0-2 > /sys/fs/cgroup/cpuset/not_cpu_3/cpuset.cpus
# This "0" is the memory node. See https://utcc.utoronto.ca/~cks/space/blog/linux/NUMAMemoryInfo
# for more information *
echo 0 > /sys/fs/cgroup/cpuset/not_cpu_3/cpuset.mems
  • 具体来说,在您的机器上,您需要查看/proc/zoneinfo/sys/devices/system/node层次结构。获取正确的节点信息留给读者自行练习。

现在我们有了我们的cgroup,我们需要创建我们的隔离CPU 3 cgroup:

cgcreate -g cpuset:cpu_3
echo 3 > /sys/fs/cgroup/cpuset/cpu_3/cpuset.cpus
# Again, the memory node(s) you want to specify.
echo 0 > /sys/fs/cgroup/cpuset/cpu_3/cpuset.mems

将所有进程/线程放在not_cpu_3 cgroup中:
for pid in $(ps -eLo pid) ; do cgclassify -g cpuset:not_cpu_3 $pid; done

简要介绍:

ps -eL k psr o psr,pid,tid,args | sort | cut -c -80

注意!当前处于睡眠状态的进程将不会移动。它们必须被唤醒,以便调度程序将它们放在不同的CPU上。要查看这一点,请在上面的列表中选择您最喜欢的睡眠进程-例如一个Web浏览器-您认为它应该在CPU 0-2上,但它仍然在3上。使用上面列表中的线程ID执行以下操作:

kill -CONT <thread_id>

例子

kill -CONT 9812

重新运行ps命令,并注意它已经移动到另一个CPU。
双重注意!某些内核线程可能无法移动,也不会移动!例如,您可能会注意到每个CPU上都有一个内核线程[kthreadd]。将进程分配给cgroup适用于用户空间进程,而不适用于内核线程。这就是多任务世界的生活。
现在,将一个进程及其所有子进程移动到控制组cpu_3:
pid=12566 # for example
cgclassify -g cpuset:cpu_3 $pid
taskset -c -p 3 $pid

如果$pid处于休眠状态,您需要唤醒它以实际进行CPU移动。

要撤消所有这些操作,只需删除您创建的cgroups即可。每个人都会被困在根cgroup中:

cgdelete -r cpuset:cpu_3
cgdelete -r cpuset:not_cpu_3

无需重新启动。 (抱歉,我不理解原帖中的第三个问题。我无法对此发表评论。)

2
如果您正在使用RHEL实例,您可以使用Tuna来实现此目的(可能也适用于其他Linux发行版,但不能确定)。它可以通过yum命令轻松安装。Tuna可用于隔离CPU核心,并动态地将在该特定CPU中运行的进程移动到相邻的CPU中。隔离CPU核心的命令如下所示: # tuna --cpus=CPU-LIST --isolate 您可以使用htop实时查看tuna如何隔离CPU核心。
最初的回答:如果您正在使用RHEL实例,您可以使用Tuna来隔离CPU核心并动态地将在该特定CPU中运行的进程移动到相邻的CPU中。隔离CPU核心的命令是: # tuna --cpus=CPU-LIST --isolate
您可以使用htop实时查看tuna如何隔离CPU核心。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接