第 4 章 CPU

CPU 意为中央处理单元,对大多数系统来说这个名字并不恰当,因为中央暗指一个,而大多数现代系统有一个以上的处理单元或者核。通常 CPUs 是一个放在一个包装中附着在主板插槽。主板的每个插槽可连接到其他 CPU 插槽、内存控制器、中断控制器以及其他外部设备。连接到操作系统的插槽是一个 CPU 及相关资源的逻辑分组。这个概念是我们要讨论的 CPU 调节中大多数问题的关键。
红帽企业版 Linux 有大量关于系统 CPU 事件的统计。这些统计数据在计划调节策略以改进 CPU 性能时很有帮助。第 4.1.2 节 “调节 CPU 性能” 中讨论了一些游泳的统计,在哪里可以找到它们以及如何就性能调节对其进行分析。

拓扑

在旧的计算机系统中,每个系统只有几个 CPU,因此将其构架称为对称多处理器(SMP)。就是说系统中的每个 CPU 对可用内存有类似(或者对称)的访问。近年来,CPU 按插槽计数已完善到可以尝试对系统中的所有内存进行对称访问,但这项技术非常昂贵。大多数高 CPU 计数系统现在才有名为非均匀内存访问(NUMA)技术而不是 SMP。
AMD 处理器很久以前就已经在其超级传输(HT)互联中采用此架构,同时 Intel 也已最其快速通道互联(QPI)设计中使用此架构。NUMA 和 SMP 调节方法各异,因为最为程序分配资源时要考虑此系统的拓扑

线程

Linux 操作系统内部到执行单元被称为线程。线程有注册上下文、栈以及在 CPU 中运行到可执行代码片段。操作系统(OS)的任务时在可用 CPU 中调度这些线程。
操作系统根据跨各个核到线程间到负载平衡最大化 CPU 使用。因为 OS 主要考虑的是要让 CPU 处于忙碌状态,它可能没有就程序性能作出最佳选择。将一个程序线程从一个 CPU 移动到其他插槽要比等待 CPU 对性能的影响更大,因为访问内存的操作在跨插槽时会变得非常缓慢。对于高性能程序,设计者最好可以决定要将其放在哪个线程中。第 4.2 节 “CPU 调度” 中讨论了如何以最佳方式分配 CPU 和内存以便最好地执行程序线程。

中断

中断(在 Linux 中被称为 IRQ)是一个不易觉察(但仍很重要)的系统事件,它可以影响程序性能。这些事件由操作系统处理,并用于外部设备以通知数据到达或者操作完成,比如系统写入或者计时器事件。
OS 或者 CPU 执行程序代码处理中断的方法不影响程序功能。但它可能影响程序的性能。本章还讨论了防止中断对程序性能产生负面影响。

4.1. CPU 拓扑

4.1.1. CPU 和 NUMA 拓扑

第一台计算机处理器是单处理机,就是说系统只有一个 CPU。让这个操作系统可以平行运行进程的幻想是希望它能够将一个 CPU 迅速从一个执行线程切换到另一个。为提高系统性能,设计者注意到采用提高时钟率提高指令执行速度的方法是有限的(通常受采用当前技术生成稳定时钟波形的限制)。在努力获得更好总体系统性能时,设计者在系统中添加了另一个 CPU,就可以让两个平行流同时运行。这个添加处理器的趋势一直延续至今。
大多数早期的单处理机系统的设计为让每个 CPU 到每个内存位置都使用同一逻辑路径(一般是平行总线)。这样每次 CPU 访问任意位置的内存时与其他系统中的 CPU 对内存的访问消耗的时间是相同的。此类架构就是我们所说的同步多处理器(SMP)系统。SMP 适合 CPU 数较少的系统,但一旦 CPU 计数超过某一点(8 或者 16),要满足对内存的平等访问所需的平行 trace 数就会使用过多的板载资源,留给外设的空间就太少。
有两个新概念整合在一起可以允许系统中有较多的 CPU:
  1. 串行总线
  2. NUMA 拓扑
串行总线是一个有很高时钟频率的单线通讯路径,以分组突发传送的方式传送数据。硬件设计者开始使用串行总线作为 CPU 之间、CPU 和内存控制器以及其他外设之间的高速互联。就是说不是要求在每个 CPU 中都有 32 和 64 个板载 trace 连接到内存子系统,现在只要一个 trace 即可,明显减少了板载空间要求。
同时,硬件设计者通过减小芯片尺寸在同样的空间中放置了更多晶体管。他们不是将独立 CPU 直接放到主板上,而是开始将其打包到处理器包中作为多核处理器。然后设计者不是为每个处理器包提供对等的内存访问,而是借助非均衡存储器访问(NUMA)策略,让每个包/插槽组合有一个或者多个专用内存区以便提供高速访问。每个插槽还有到另一个插槽的互联以便提供对其他插槽内存的低速访问。
作为简单的 NUMA 示例,假设我们有一个双插槽主板,其中每个插槽都有四核。就是说该系统中的 CPU 总数为 8,每个插槽有 4 个。每个插槽还附带 4GB 内存条,内存总数为 8GB。在这个示例中 CPU 0-3 在插槽 0 中,CPU 4-7 在插槽 1 中。这个示例中的每个插槽都对应一个 NUMA 代码。
CPU 0 访问内存条 0 大约需要三个时钟周期:一个周期是将地址发给内存控制器,一个周期是设置对该内存位置的访问,一个周期是读取或者写入到该位置。但 CPU 4 可能需要 6 个时钟周期方可访问内存的同一位置,因为它位于不同的插槽,必须经过两个内存控制器:插槽 1 中的本地内存控制器和插槽 0 中的远程内存控制器。如果在那个位置出现竞争(即如果有一个以上 CPU 同时尝试访问同一位置),内存控制器需要对该内存进行随机且连续的访问,所以内存访问所需时间会较长。添加缓存一致性(保证本地 CPU 缓存包含同一内存位置的相同数据)会让此过程更为复杂。
最新高端处理器,比如 Intel 的 Xeon 和 AMD 的 Opteron 都有 NUMA 拓扑。AMD 处理器使用我们所说的超传输(或称 HT)互联,而 Intel 使用名为快速路径(或称 QPI)的互联。根据其物理连接到其他互联、内存或者外设的情况该互联有所不同,但实际上他们就是可允许从另一台连接的设备对一个连接的设备进行透明访问的开关。在这种情况下,透明指的是使用该互联没有特别的编程 API 要求,而不是“零成本”选项。
因为系统架构千变万化,因此具体指定由于访问非本地内存所致性能代偿是不切实节的。我们可以说每个跨互联的中继段多少会产生一些相对恒定的性能代偿,因此参考距当前 CPU 两个互联的内存位置时至少会产生 2N + 内存周期时间单位访问时间,其中 N 是每个中继段的代偿。
有这个性能代偿后,对性能敏感的程序应避免常规访问 NUMA 拓扑系统中的远程内存。应将程序设定为使用特定的节点,并从那个节点为其分配内存。
要做到这一点,需要了解程序的一些情况:
  1. 系统使用什么拓扑
  2. 该程序目前在哪里执行?
  3. 最近的内存条在哪里?

4.1.2. 调节 CPU 性能

阅读本小节了解如何调整出更好的 CPU 性能,本小节中还介绍了几个用于此目的的工具。
NUMA 最初是用于将单一处理器连接到多个内存条中。因为 CPU 制造商改进了其工艺并缩小了芯片尺寸,因此可在一个包装中包括多个 CPU 核。这些 CPU 核以集群形式寻租以便每个核都有相同的访问本地内存条的时间,同时可在核之间共享缓存。但每个核、内存以及缓存中跨互联的‘中继段’都有一个小的性能代偿。
图 4.1 “NUMA 拓扑中的本地和远程内存访问” 中的示例系统包括两个 NUMA 节点。每个节点有 4 个 CPU,一个内存条和一个内存控制器,节点中的任意 CPU 都可以直接访问那个节点中的内存条。根据节点 1 中的箭头指示执行步骤如下:
  1. CPU(0-3)给出到本地内存控制器的内存地址。
  2. 内存控制器设置对内存地址的访问。
  3. CPU 在那个内存地址执行读取或者写入操作。
The CPU icon used in this image is part of the Nuvola 1.0 (KDE 3.x icon set), and is held under the LGPL-2.1: http://www.gnu.org/licenses/lgpl-2.1.html

图 4.1. NUMA 拓扑中的本地和远程内存访问

但如果一个节点中的 CPU 需要访问不同 NUMA 节点的内存条中的代码,则它要使用的路径就不那么直接:
  1. CPU(0-3)给出到本地内存控制器的远程地址。
    1. 会将对那个远程内存地址的 CPU 请求传递给远程内存控制器,到该节点的本地控制器包含那个内存地址。
  2. 远程内存控制器设置对远程内存地址的访问。
  3. CPU 在那个远程内存地址执行读取或者写入操作。
每个动作都需要通过多个内存控制器,这样访问在尝试访问远程内存地址时时间会延长两倍以上。因此多核系统中主要性能考量是保证以最有效的方式进行信息传递,即通过最短最迅速的路径。
要为优化 CPU 性能配置程序,您需要了解:
  • 系统的拓扑(组件是如何连接的),
  • 执行程序的核,以及
  • 最接近的内存条位置。
红帽企业版 Linux 6 附带大量可以帮助您找到这个信息并根据您的发现调整系统的工具。以下小节对概述了用于 CPU 性能调节有帮助的工具。

4.1.2.1. 使用 taskset 设置 CPU 亲和性

taskset 搜索并设定运行进程的 CPU 亲和性(根据进程 ID)。它还可用于启动给定 CPU 亲和性的进程,这样就可将指定的进程与指定的 CPU 或者一组 CPU 捆绑。但 taskset 不保证本地内存分配。如果您需要本地内存分配的额外性能利益,我们建议您使用 numactl,而不是 taskset。详情请查看 第 4.1.2.2 节 “使用 numactl 控制 NUMA 策略”
CPU 亲和性使用位掩码表示。最低位对应第一个逻辑 CPU,且最高位对应最后一个逻辑 CPU。这些掩码通常是十六进制,因此 0x00000001 代表处理器 0,0x00000003 代表处理器 3 和 1。
要设定运行进程的 CPU 亲和性,请执行以下命令,使用处理器或者您要捆绑到的处理器掩码替换 mask,使用您要更改亲和性的进程的进程 ID 替换 pid
# taskset -p mask pid
要启动给定亲和性的进程,请运行以下命令,使用处理器或者您要捆绑的处理器的掩码替换 mask,使用程序、选项以及您要运行的程序参数替换 program
# taskset mask -- program
与其将处理器指定为位码,您还可以使用 -c 选项提供逗号分开的独立处理器,或者一组处理器列表,类似如下:
# taskset -c 0,5,7-9 -- myprogram
有关 taskset 的详情请参考 man page:man taskset

4.1.2.2. 使用 numactl 控制 NUMA 策略

numactl 使用指定的调度或者内存放置策略运行进程。所选策略是为那个进程及其所有子进程设定。numactl 还可以为共享内存片段或者文件设定永久策略,并设定 CPU 亲和性和进程的内存亲和性。它使用 /sys 文件系统决定系统拓扑。
/sys 文件系统包含有关 CPU、内存和外设是如何通过 NUMA 互联连接的。特别是 /sys/devices/system/cpu 目录中包含有关系统的 CPU 是如何互相连接的信息。/sys/devices/system/node 目录包含有关系统中 NUMA 节点以及那些节点间相对距离的信息。
在 NUMA 系统中,处理器和内存条之间的距离越大,处理器访问那个内存条的速度就越慢。应将对性能敏感的程序配置为可以从最接近的内存条分配内存。
还应将对性能敏感的程序配置为执行一组核,特别是在多线程程序的情况下。因为以及缓存一般都很小,如果在一个核中执行多个线程,每个线程可有可能逐出由之前线程访问的缓冲的数据。当操作系统尝试在这些线程间执行多任务,且线程继续逐出每个其他的缓存的数据时,则其执行时间的很大比例将用于缓存线替换。这个问题也称缓存贬值。因此建议您将多线程的程序捆绑到节点而不是单一核,因为这样可以让线程在多个层级(第一、第二和最后以及缓存)共享缓存线,并尽量减小缓存填充操作的需要。但如果所有线程都访问同一缓存的数据,则将程序捆绑到单一核可能获得高性能。
numactl 可让您将程序捆绑到特定核或者 NUMA 节点,同时要将内存分配到与那个程序关联的核或者一组核。numactl 提供的一些有用选项有:
--show
显示当前进程的 NUMA 策略设置。这个参数不需要进一步的参数,且可按以下方式使用:numactl --show
--hardware
显示系统中可用节点清单。
--membind
只从指定节点分配内存。当使用这个参数时,如果这些节点中的内存不足则分配会失败。这个参数的用法为 numactl --membind=nodes program,其中 nodes 是您要从中分配内存的节点列表,program 是要从那个节点分配内存的程序。节点号可以采用用逗号分开的列表、范围或者两者的结合方式提供。有关详情请参考 numactl man page: man numactl
--cpunodebind
只执行属于指定节点的 CPU 中的命令(及其子进程)。这个参数的用法为 numactl --cpunodebind=nodes program,其中 nodes 是指定程序(program)要捆绑的 CPU 所属节点列表。节点号可以采用用逗号分开的列表、范围或者两者的结合方式提供。有关详情请参考 numactl man page: man numactl
--physcpubind
只执行指定 CPU 中的命令(及其子进程)。这个参数的用法为 numactl --physcpubind=cpu program,其中 cpu 是用逗号分开的物理 CPU 号列表,这些数据在 /proc/cpuinfo 的 processor 字段中显示,program 是应只在哪些 CPU 中执行的程序。还要将 CPU 指定为与当前 cpuset 关联。详情请参考 numactl man page:man numactl
--localalloc
指定永远要在当前节点中分配的内存。
--preferred
在可能的情况下分配到指定节点中的内存。如果内存无法分配到指定的节点,则返回其他节点。这个选项只能有一个节点号,例如:numactl --preferred=node。详情请参考 numactl man page:man numactl
numactl 软件包中包含的 libnuma 程序库为内核支持的 NUMA 策略提供编程界面。这比 numactl 程序可更详细地调节系统。有关详情请参考 man page:man numa(3)

4.1.3. numastat

重要

之前 numastat 工具是由 Andi Kleen 编写的 Perl 脚本。在红帽企业版 Linux 6.4 中对其进行了重大修改。
虽然默认命令(numastat,没有任何选项或者参数)可保持与之前版本的严格兼容性,但请注意在这个命令中使用选项或者参数会极大更改输出结果内容及其格式。
numastat 显示进程以及每个 NUMA 节点中操作系统的内存统计数据(比如分配成功数和失败数)。默认情况下,numastat 显示每个节点中的以下事件分类所占内存页数。
numa_missnuma_foreign 值表示最佳 CPU 性能。
这个更新的 numastat 版本还显示是在系统间分配进程内存,还是使用 numactl 在具体的节点中集中使用。
numastat 输出结果与每个 CPU top 输出结果对比确定进程线程在分配了内存的同一节点中运行。

默认跟踪分类

numa_hit
为这个节点成功的分配尝试数。
numa_miss
由于在目的节点中内存较低而尝试为这个节点分配到另一个节点的数目。每个 numa_miss 事件都在另一个节点中有对应的 numa_foreign 事件。
numa_foreign
最初要为这个节点但最后分配个另一个节点的分配数。每个 每个 numa_foreign 事件都在另一个节点中有对应的 numa_miss 事件。
interleave_hit
成功分配给这个节点的尝试交错策略数。
local_node
这个节点中的进程成功在这个节点中分配内存的次数。
other_node
这个节点中的进程成功在另一个节点中分配内存的次数。
提供任意以下选项可将显示内存单位更改为 MB(四舍五入为两位十进制数),并将其他具体 numastat 行为更改如下。
-c
横向紧凑地显示信息表。这对有大量 NUMA 节点的系统很有用,但栏宽度以及栏间空间有时无法预测。使用这个选项时,会将内存值四舍五入到最接近的 MB 数。
-m
显示每个节点中系统范围内的内存使用信息,类似 /proc/meminfo 中的信息。
-n
显示与原始 numastat 命令类似的信息(numa_hit, numa_miss, numa_foreign, interleave_hit, local_node, and other_node),采用更新的格式,使用 MB 作为测量单位。
-p pattern
为指定的模式显示每个节点的内存信息。如果 pattern 值由数字组成,numastat 假设它是一个数字进程识别符。否则 numastat 会为指定的模式搜索进程命令行。
假设在 -p 选项值后输入的命令行参数是过滤器的附加模式。附加模式要扩大而不是缩小过滤范围。
-s
以降序模式排列显示的数据以便让最大内存消耗者(根据 total 栏)列在首位。
您也可以指定 node,并根据 node 栏排列表格。当使用这个选项时,node 值后必须立即跟上一个 -s 选项,如下所示:
numastat -s2
不要在该选项及其数值之间有空格。
-v
显示更详细的信息。就是说多进程的进程信息会为每个进程显示详细的信息。
-V
显示 numastat 版本信息。
-z
省略表格显示的信息中数值为 0 的行和列。注:有些接近 0 的值都四舍五入为 0 以方便显示,这些数值不会在显示的输出结果中省略。

4.1.4. NUMA 亲和性管理守护进程(numad

numad 是一个自动 NUMA 亲和性管理守护进程,它监控系统中的 NUMA 拓扑以及资源使用以便动态提高 NUMA 资源分配和管理(以及系统性能)。
根据系统负载,numad 可对基准性能有 50% 的提高。要达到此性能优势,numad 会周期性访问 /proc 文件系统中的信息以便监控每个节点中的可用系统资源。该守护进程然后会尝试在有足够内存和 CPU 资源的 NUMA 节点中放置大量进程已优化 NUMA 性能。目前进程管理阈为至少是一个 CPU 的 50%,且至少有 300 MB 内存。numad 会尝试维护资源使用水平,并在需要时通过在 NUMA 节点间移动进程平衡分配。
numad 还提供预布置建议服务,您可以通过各种任务管理系统进行查询以便提供 CPU 起始捆绑以及进程内存资源的支持。这个预布置建议服务无论系统中是否运行 numad 都可以使用。有关为预布置建议使用 -w 选项的详情请参考 man page:man numad

4.1.4.1. numad 的优点

numad 主要可让长期运行消耗大量资源的系统受益,特别是当这些进程是包含在总系统资源子集中时尤为突出。
numad 还可以让消耗相当于多个 NUMA 节点资源的程序受益,但 numad 提供的优势可被由于系统增长而消耗的资源比例抵消。
numad 不太可能在进程只运行几分钟或者不会消耗很多资源时改进性能。有连续不可预测内存访问的系统,比如大型内存中的数据库也不大可能从 numad 使用中受益。

4.1.4.2. 操作模式

注意

内核内存审核统计数据之间可能会在大规模整合后产生冲突。如果是这样则可能会在 KSM 数据库处整合大量内存时让 numad 困惑。将来的 KSM 守护进程版本可能会更好地识别 NUMA。但目前,如果您的系统有大量剩余内存,则可能需要您关闭并禁用 KSM 守护进程方可获得较高的性能。
numad 有两种使用方法:
  • 作为服务使用
  • 作为可执行文件使用
4.1.4.2.1. 将 numad 作为服务使用
numad 服务运行时,它将会尝试根据其负载动态调节系统。
要启动该服务,请运行:
# service numad start
要让该服务在重启后仍保留,请运行:
# chkconfig numad on
4.1.4.2.2. 将 numad 作为可执行文件使用
要将 numad 作为可执行文件使用,请运行:
# numad
numad 将运行直到将其停止。在它运行时,其活动将被记录到 /var/log/numad.log 文件中。
要将 numad 限制为管理具体进程,请使用以下选项启动它。
# numad -S 0 -p pid
-p pid
在明确包含列表中添加指定的 pidnumad 进程将管理这个指定的进程直到达到其重要阈值。
-S mode
-S 参数指定扫描的进程类型。如示将其设定为 0 则将 numad 管理明确规定到所包含的进程。
要停止 numad,请运行:
# numad -i 0
停止 numad 不会删除它对改进 NUMA 亲和性所做的更改。如果系统使用大量更改,再次运行 numad 将调整清河性以便在新条件下提高性能。
有关 numad 可用选项的详情请参考 numad man page:man numad

为了尽快向用户提供最新的信息,本文档可能会包括由机器自动从英文原文翻译的内容。如需更多信息,请参阅此说明。