Skip to content

第四十四章:亲和性

结丹中期

涉及内核源码:

林小源在竞技场中行走时,看到了一个奇怪的景象。

一个进程的光团被一条金色的锁链拴在 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。"


c
/*
 * 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");

道藏笔记

内核启示

CPU 亲和性让进程可以"选择"自己的 CPU。

CPU 亲和性的系统调用:

  • sched_setaffinity(pid, mask) — 设置 CPU 亲和性
  • sched_getaffinity(pid, mask) — 获取 CPU 亲和性

亲和性掩码():

  • 每个 CPU 对应一个 bit
  • bit 为 1 表示可以在该 CPU 上运行

CPU 亲和性的用途:

  1. 性能优化 — 绑定到特定 CPU,利用缓存
  2. 实时性 — 绑定到隔离的 CPU,避免干扰
  3. NUMA 优化 — 绑定到本地 NUMA 节点
  4. 调试 — 限制进程在特定 CPU 上运行

亲和性与负载均衡的平衡:

  • 亲和性限制了进程的迁移范围
  • 负载均衡在亲和性允许的范围内进行
  • 亲和性优先于负载均衡

CPU 亲和性是"自由"与"效率"的权衡。


破关试炼

亲和性之试

本章讲 CPU 亲和性时,用户用来限制进程可运行 CPU 集合的接口是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道