第四十四章:亲和性
结丹中期涉及内核源码:
一
林小源在竞技场中行走时,看到了一个奇怪的景象。
一个进程的光团被一条金色的锁链拴在 CPU 2 的石台上。它试图向其他石台移动,但每次都被锁链拉了回来。
"那是什么?"林小源指着那条锁链问。
一个穿着金色铠甲的守卫走了过来。他的胸口刻着一个 cpumask 的符号——一串二进制数字,每一位代表一个 CPU。
"CPU 亲和性。"守卫说,"这个进程通过 设置了自己的亲和性掩码——它只能在 CPU 2 和 CPU 3 上运行。"
"为什么要做这种限制?"
守卫走到石台旁,指着石台表面的符文:"有几种原因。第一,性能优化——绑定到特定 CPU,利用缓存。第二,实时性——绑定到隔离的 CPU,避免其他进程干扰。第三,NUMA 优化——绑定到本地 NUMA 节点,减少远程内存访问。第四,调试——限制进程在特定 CPU 上运行,复现特定的并发问题。"
林小源看着那条锁链:"但限制不是会降低灵活性吗?"
"有时候,限制反而带来性能。"守卫说,"想象一个数据库进程——它需要频繁访问内存中的数据。如果它被频繁迁移到不同的 CPU,缓存会不断失效,性能会严重下降。绑定到特定 CPU,虽然限制了迁移范围,但保证了缓存的命中率。"
二
林小源在守卫的指导下,开始研究亲和性掩码。
"亲和性掩码是一个 cpumask,"守卫说,"每个 CPU 对应一个 bit。bit 为 1 表示可以在该 CPU 上运行,bit 为 0 表示不能。"
他在空中画了一个掩码:0b00001100——表示进程只能在 CPU 2 和 CPU 3 上运行。
"设置亲和性需要 能力吗?"林小源问。
"不一定。"守卫说,"普通进程可以设置自己的亲和性,也可以设置子进程的亲和性。但如果要修改其他进程的亲和性,需要 能力。"
林小源想起了优先级的设计:"降低自己是自由的,提高自己需要特权。亲和性也是一样?"
"类似。"守卫说,"你可以限制自己的活动范围,但不能随意限制别人的活动范围。这是安全设计。"
"还有一个重要的点,"守卫补充道,"亲和性只是'建议',不是'强制'。如果所有亲和的 CPU 都很忙,进程可能会等待——或者在极端情况下,内核可能会忽略亲和性设置。"
三
林小源在竞技场中走动,观察着不同进程的亲和性设置。
他注意到某些高性能应用——比如数据库和网络服务器——都有严格的亲和性设置。它们的线程被绑定到特定的 CPU 上,不会被迁移到其他 CPU。
"这些应用为什么这么执着于亲和性?"林小源问。
守卫走过来:"因为它们的工作负载对缓存和 NUMA 非常敏感。一个数据库查询可能需要访问大量内存中的数据——如果这些数据在 CPU 0 的缓存中,而进程被迁移到 CPU 1,缓存就失效了。CPU 1 需要重新从内存中加载数据——这可能需要几百个时钟周期。"
"所以亲和性和负载均衡是矛盾的。"
"对。"守卫说,"亲和性限制了进程的迁移范围,而负载均衡需要迁移进程来平衡负载。内核通过'亲和性优先'的策略来解决这个矛盾——在亲和性允许的范围内进行负载均衡。"
林小源想起了自己在炼气期学到的一个道理:自由和秩序需要平衡。亲和性是"秩序"——限制了进程的活动范围。负载均衡是"自由"——让进程在各 CPU 之间流动。两者的平衡,才是系统高效运行的关键。
"还有一种情况,"守卫说,"某些实时应用会将线程绑定到隔离的 CPU 上——这些 CPU 不运行任何其他进程。这保证了实时任务的延迟是可预测的。"
"隔离。"林小源喃喃道。
"对。"守卫说,"在内核中,'隔离'也是一种保护。就像你在修炼时,需要一个安静的环境来闭关——CPU 隔离就是给实时任务一个'安静'的 CPU。"
/*
* CPU 亲和性 (CPU Affinity):
* 进程可以设置自己只能在哪些 CPU 上运行。
*
* sched_setaffinity(pid, mask) — 设置 CPU 亲和性
* sched_getaffinity(pid, mask) — 获取 CPU 亲和性
*
* 亲和性掩码 (cpumask):
* 每个 CPU 对应一个 bit
* bit 为 1 表示可以在该 CPU 上运行
*
* 用途:
* 1. 性能优化:绑定到特定 CPU,利用缓存
* 2. 实时性:绑定到隔离的 CPU,避免干扰
* 3. NUMA 优化:绑定到本地 NUMA 节点
* 4. 调试:限制进程在特定 CPU 上运行
*/
typedef unsigned long cpumask_t;
void cpumask_set_cpu(cpumask_t *mask, int cpu) {
*mask |= (1UL << cpu);
}
void cpumask_clear_cpu(cpumask_t *mask, int cpu) {
*mask &= ~(1UL << cpu);
}
int cpumask_test_cpu(cpumask_t mask, int cpu) {
return (mask >> cpu) & 1;
}
printf("=== CPU 亲和性 ===\n\n");
/* 8 个 CPU 的系统 */
cpumask_t affinity;
/* 进程 A: 绑定到 CPU 0-3 */
affinity = 0;
cpumask_set_cpu(&affinity, 0);
cpumask_set_cpu(&affinity, 1);
cpumask_set_cpu(&affinity, 2);
cpumask_set_cpu(&affinity, 3);
printf("进程 A 的 CPU 亲和性: 0x%lx\n", affinity);
printf(" 可以在 CPU: ");
for (int i = 0; i < 8; i++) {
if (cpumask_test_cpu(affinity, i))
printf("%d ", i);
}
printf("\n\n");
/* 进程 B: 绑定到 CPU 4-7 */
affinity = 0;
for (int i = 4; i < 8; i++)
cpumask_set_cpu(&affinity, i);
printf("进程 B 的 CPU 亲和性: 0x%lx\n", affinity);
printf(" 可以在 CPU: ");
for (int i = 0; i < 8; i++) {
if (cpumask_test_cpu(affinity, i))
printf("%d ", i);
}
printf("\n\n");
printf("--- 亲和性的用途 ---\n");
printf("1. 性能优化:\n");
printf(" 绑定到特定 CPU,利用 CPU 缓存\n");
printf(" 减少缓存失效\n\n");
printf("2. 实时性:\n");
printf(" 绑定到隔离的 CPU,避免其他进程干扰\n");
printf(" 保证实时任务的延迟\n\n");
printf("3. NUMA 优化:\n");
printf(" 绑定到本地 NUMA 节点\n");
printf(" 减少远程内存访问\n\n");
printf("4. 调试:\n");
printf(" 限制进程在特定 CPU 上运行\n");
printf(" 复现特定的并发问题\n");
printf("\n--- 亲和性的限制 ---\n");
printf("亲和性只是\"建议\",不是\"强制\"\n");
printf("如果所有亲和的 CPU 都很忙,进程可能会等待\n");
printf("或者内核可能会忽略亲和性设置\n");#include <stdio.h>
#include <string.h>
/*
* CPU 亲和性 (CPU Affinity):
* 进程可以设置自己只能在哪些 CPU 上运行。
*
* sched_setaffinity(pid, mask) — 设置 CPU 亲和性
* sched_getaffinity(pid, mask) — 获取 CPU 亲和性
*
* 亲和性掩码 (cpumask):
* 每个 CPU 对应一个 bit
* bit 为 1 表示可以在该 CPU 上运行
*
* 用途:
* 1. 性能优化:绑定到特定 CPU,利用缓存
* 2. 实时性:绑定到隔离的 CPU,避免干扰
* 3. NUMA 优化:绑定到本地 NUMA 节点
* 4. 调试:限制进程在特定 CPU 上运行
*/
typedef unsigned long cpumask_t;
void cpumask_set_cpu(cpumask_t *mask, int cpu) {
*mask |= (1UL << cpu);
}
void cpumask_clear_cpu(cpumask_t *mask, int cpu) {
*mask &= ~(1UL << cpu);
}
int cpumask_test_cpu(cpumask_t mask, int cpu) {
return (mask >> cpu) & 1;
}
int main() {
printf("=== CPU 亲和性 ===\n\n");
/* 8 个 CPU 的系统 */
cpumask_t affinity;
/* 进程 A: 绑定到 CPU 0-3 */
affinity = 0;
cpumask_set_cpu(&affinity, 0);
cpumask_set_cpu(&affinity, 1);
cpumask_set_cpu(&affinity, 2);
cpumask_set_cpu(&affinity, 3);
printf("进程 A 的 CPU 亲和性: 0x%lx\n", affinity);
printf(" 可以在 CPU: ");
for (int i = 0; i < 8; i++) {
if (cpumask_test_cpu(affinity, i))
printf("%d ", i);
}
printf("\n\n");
/* 进程 B: 绑定到 CPU 4-7 */
affinity = 0;
for (int i = 4; i < 8; i++)
cpumask_set_cpu(&affinity, i);
printf("进程 B 的 CPU 亲和性: 0x%lx\n", affinity);
printf(" 可以在 CPU: ");
for (int i = 0; i < 8; i++) {
if (cpumask_test_cpu(affinity, i))
printf("%d ", i);
}
printf("\n\n");
printf("--- 亲和性的用途 ---\n");
printf("1. 性能优化:\n");
printf(" 绑定到特定 CPU,利用 CPU 缓存\n");
printf(" 减少缓存失效\n\n");
printf("2. 实时性:\n");
printf(" 绑定到隔离的 CPU,避免其他进程干扰\n");
printf(" 保证实时任务的延迟\n\n");
printf("3. NUMA 优化:\n");
printf(" 绑定到本地 NUMA 节点\n");
printf(" 减少远程内存访问\n\n");
printf("4. 调试:\n");
printf(" 限制进程在特定 CPU 上运行\n");
printf(" 复现特定的并发问题\n");
printf("\n--- 亲和性的限制 ---\n");
printf("亲和性只是\"建议\",不是\"强制\"\n");
printf("如果所有亲和的 CPU 都很忙,进程可能会等待\n");
printf("或者内核可能会忽略亲和性设置\n");
return 0;
}道藏笔记
内核启示
CPU 亲和性让进程可以"选择"自己的 CPU。
CPU 亲和性的系统调用:
sched_setaffinity(pid, mask)— 设置 CPU 亲和性sched_getaffinity(pid, mask)— 获取 CPU 亲和性
亲和性掩码():
- 每个 CPU 对应一个 bit
- bit 为 1 表示可以在该 CPU 上运行
CPU 亲和性的用途:
- 性能优化 — 绑定到特定 CPU,利用缓存
- 实时性 — 绑定到隔离的 CPU,避免干扰
- NUMA 优化 — 绑定到本地 NUMA 节点
- 调试 — 限制进程在特定 CPU 上运行
亲和性与负载均衡的平衡:
- 亲和性限制了进程的迁移范围
- 负载均衡在亲和性允许的范围内进行
- 亲和性优先于负载均衡
CPU 亲和性是"自由"与"效率"的权衡。
亲和性之试
本章讲 CPU 亲和性时,用户用来限制进程可运行 CPU 集合的接口是什么?