Linux 中 CPU 亲和性
1. Linux 中 CPU 亲和性(affinity)
1.1. 准备知识
- 并行: 两件 (多件) 事情在同一时刻一起发生。
- 并发: 两件 (多件) 事情在同一时刻只能有一个发生, 由于 CPU 快速切换, 从而给人的感觉是同时进行。
- 单核多线程: 单核 CPU 上运行多线程, 同一时刻只有一个线程在跑, 系统进行线程切换, 系统给每个线程分配时间片来执行, 看起来就像是同时在跑, 但实际上是每个线程跑一点点就换到其它线程继续跑。
- 多核多线程: 每个核上各自运行线程, 同一时刻可以有多个线程同时在跑。
- 物理 CPU: 机器上实际安装的 CPU 个数, 比如说你的主板上安装了一块 8 核 CPU, 那么物理 CPU 个数就是 1 个, 所以物理 CPU 个数就是主板上安装的 CPU 个数。
- 逻辑 CPU: 一般情况, 我们认为一颗 CPU 可以有多个核, 加上 intel 的超线程技术(HT), 可以在逻辑上再分一倍数量的 CPU core 出来。
- 超线程技术(Hyper-Threading): 就是利用特殊的硬件指令, 把两个逻辑 CPU 模拟成两个物理 CPU, 实现多核多线程。我们常听到的双核四线程 / 四核八线程指的就是支持超线程技术的 CPU。
逻辑 CPU 数量 = 物理 CPU 数量 x CPU cores x 2(如果支持并开启 HT)
比如: 你的电脑安装了一块 4 核 CPU, 并且支持且开启了超线程 (HT) 技术, 那么逻辑 CPU 数量 = 1 × 4 × 2 = 8
1.2. CPU 的亲和性(affinity)
简单地说, CPU 亲和性 (affinity) 就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。
软亲和性 (affinity: 就是进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他处理器, Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性, 这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的, 因为进程迁移的频率小就意味着产生的负载小。
2.6 版本的 Linux 内核还包含了一种机制, 它让开发人员可以编程实现硬 CPU 亲和性 (affinity)。这意味着应用程序可以显式地指定进程在哪个(或哪些) 处理器上运行。
1.3. 什么是 Linux 内核硬亲和性(affinity)?
硬亲和性(affinity): 简单来说就是利用 linux 内核提供给用户的 API, 强行将进程或者线程绑定到某一个指定的 cpu 核运行。
在 Linux 内核中, 所有的进程都有一个相关的数据结构, 称为 task_struct 。这个结构非常重要, 原因有很多; 其中与 亲和性 (affinity) 相关度最高的是 cpus_allowed 位掩码。这个位掩码由 n 位组成, 与系统中的 n 个逻辑处理器一一对应。 具有 4 个物理 CPU 的系统可以有 4 位。如果这些 CPU 都启用了超线程, 那么这个系统就有一个 8 位的位掩码。
如果为给定的进程设置了给定的位, 那么这个进程就可以在相关的 CPU 上运行。因此, 如果一个进程可以在任何 CPU 上运行, 并且能够根据需要在处理器之间进行迁移, 那么位掩码就全是 1。实际上, 这就是 Linux 中进程的缺省状态。
Linux 内核 API 提供了一些方法, 让用户可以修改位掩码或查看当前的位掩码:
- sched_set_affinity() (用来修改位掩码)
- sched_get_affinity() (用来查看当前的位掩码)
注意, cpu_affinity 会被传递给子线程, 因此应该适当地调用 sched_set_affinity。
1.4. 为什么应该使用硬亲和性(affinity)?
通常 Linux 内核都可以很好地对进程进行调度, 在应该运行的地方运行进程(这就是说, 在可用的处理器上运行并获得很好的整体性能)。内核包含了一些用来检测 CPU 之间任务负载迁移的算法, 可以启用进程迁移来降低繁忙的处理器的压力。
一般情况下, 在应用程序中只需使用缺省的调度器行为。然而, 您可能会希望修改这些缺省行为以实现性能的优化。让我们来看一下使用硬亲和性(affinity) 的 3 个原因。
- 原因 1. 有大量计算要做
基于大量计算的情形通常出现在科学和理论计算中, 但是通用领域的计算也可能出现这种情况。一个常见的标志是您发现自己的应用程序要在多处理器的机器上花费大量的计算时间。
- 原因 2. 提高 Cache 命中率
在多核运行的机器上, 每个 CPU 都有自己的缓存, 缓存着进程使用的信息, 而进程可能会被 OS 调度到其他 CPU 上, 如此一来 CPU Cache 命中率就低了。当绑定 CPU 后, 程序就会一直在指定的 cpu 跑, 不会由 OS 调度到其他 CPU 上, 提高 CPU Cache 命中率。
- 原因 3. 正在运行时间敏感的、决定性的进程
我们对 CPU 亲和性 (affinity) 感兴趣的最后一个原因是实时 (对时间敏感的) 进程。例如, 您可能会希望使用硬亲和性 (affinity) 来指定一个 8 路主机上的某个处理器, 而同时允许其他 7 个处理器处理所有普通的系统调度。这种做法确保长时间运行、对时间敏感的应用程序可以得到运行, 同时可以允许其他应用程序独占其余的计算资源。
1.5. 如何设置硬亲和性(affinity)?
1.5.1. 用户态进程与 CPU 绑定
函数原型:
int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
该函数设置进程为 pid 的这个进程, 让它运行在 mask 所设定的 CPU 上. 如果 pid 的值为 0, 则表示指定的是当前进程, 使当前进程运行在 mask 所设定的那些 CPU 上。第二个参数 cpusetsize 是 mask 所指定的数的长度. 通常设定为 sizeof(cpu_set_t)。如果当前 pid 所指定的进程此时没有运行在 mask 所指定的任意一个 CPU 上, 则该指定的进程会从其它 CPU 上迁移到 mask 的指定的一个 CPU 上运行。
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
该函数获得 pid 所指示的进程的 CPU 位掩码, 并将该掩码返回到 mask 所指向的结构中。即获得指定 pid 当前可以运行在哪些 CPU 上。同样, 如果 pid 的值为 0。也表示的是当前进程。
宏定义:
void CPU_ZERO (cpu_set_t *set);
对 CPU 集 set 进行初始化, 将其设置为空集。
void CPU_SET (int cpu, cpu_set_t *set);
将指定的 cpu 加入 CPU 集 set 中
void CPU_CLR (int cpu, cpu_set_t *set);
将指定的 cpu 从 CPU 集 set 中删除。
int CPU_ISSET (int cpu, const cpu_set_t *set);
如果 cpu 是 CPU 集 set 的一员, 这个宏就返回一个非零值(true), 否则就返回零(false)。
相关函数:
long sysconf(int name);
参数 name 取值:
_SC_NPROCESSORS_CONF: 查看 cpu 的个数
_SC_NPROCESSORS_ONLN: 查看正在使用的 cpu 个数
_SC_PAGESIZE: 查看缓存内存页面的大小
_SC_PHYS_PAGES: 查看内存的总页数
_SC_AVPHYS_PAGES: 查看可以利用的总页数
_SC_LOGIN_NAME_MAX: 查看最大登录名长度
_SC_HOST_NAME_MAX: 查看最大主机名长度
_SC_OPEN_MAX: 每个进程运行时打开的文件数目
_SC_CLK_TCK: 查看每秒中跑过的运算速率
sysconf(_SC_PAGESIZE) * sysconf(_SC_PHYS_PAGES) : 计算内存大小
伪代码:
cpu_set_t mask;
/* 初始化 set 集, 将 set 设置为空 */
CPU_ZERO(&mask);
/* 依次将 0、1 号 cpu 加入到集合 */
CPU_SET(0, &mask);
CPU_SET(1, &mask);
/* 将当前进程绑定到 cpu */
sched_setaffinity(0, sizeof(mask), &mask);
1.5.2. 用户态线程与 CPU 绑定
- 函数原型
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,cpu_set_t *cpuset);
相关函数:
pthread_t pthread_self(void);
功能是获得线程自身的 ID。
伪代码:
cpu_set_t mask;
/* 初始化 set 集, 将 set 设置为空 */
CPU_ZERO(&mask);
/* 依次将 0、1 号 cpu 加入到集合 */
CPU_SET(0, &mask);
CPU_SET(1, &mask);
/* 将当前线程程绑定到 cpu */
pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask);