在Linux系统中,进程的调度切换是由内核自动完成的,在多核CPU上,进程有可能在不同的CPU核上来回切换执行,这对CPU的缓存不是很有利。为什么呢?先看一张 Intel i5 CPU 的缓存简单示意图:
在多核CPU结构中,每个核心有各自的L1、L2缓存,而L3缓存是共用的。如果一个进程在核心间来回切换,各个核心的缓存命中率就会受到影响。相反如果进程不管如何调度,都始终可以在一个核心上执行,那么其数据的L1、L2 缓存的命中率可以显著提高。
在 Linux 系统里,可以使用 CPU_* 系列函数和 sched_setaffinity()
可以实现绑定,具体步骤如下:
#define _GNU_SOURCE
cpu_set_t
,然后用 CPU_ZERO()
初始化bit数据:cpu_set_t mask;
CPU_ZERO(&mask);
cpu_set_t
其实是一个bit串,每个bit表示进程是否要与某个CPU核绑定。
CPU_SET()
来设置cpu_set_t中相应的bit位,比如想让进程只在核心1或核心5上执行:CPU_SET(1, &mask);
CPU_SET(5, &mask);
sched_setaffinity
完成实际的绑定:sched_setaffinity(0, sizeof(cpu_set_t), &mask);
设置起来并不难。那怎么验证我们的绑定真的起作用了呢?我们来做个实验:
假定有一台双核机器,这段程序我们起了20个进程,从0开始每个进程分配一个进程号(注意是这里值我们自己起的进程号,不是进程pid),奇数进程号绑定绑定在 Core 0上执行,偶数号的进程绑定在 Core 1上执行。
我们用for
让进程循环,用 sched_getcpu()
函数获得当前进程运行在哪个CPU核心上,每次for
循环检查下进程是否真的在分配的核心执行。
#define _GNU_SOURCE
#include
#include
#include
#include
void run(int c, int n) {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(n, &mask);
sched_setaffinity(0, sizeof(cpu_set_t), &mask);
int i;
for (i = 0; i != 10000; i++) {
printf("%d-%d\n", c, sched_getcpu());
}
}
int main()
{
int i;
for (i = 0; i != 20; i++) {
int pid = fork();
if (pid == 0) {
run(i, i % 2);
exit(0);
}
}
}
执行上面的程序,就会打印每个进程绑定的CPU核号,进程与核号的关系肯定不会变。如果把 sched_setaffinity()
注释掉,CPU进程就失去绑定。
设置了进程与CPU绑定后,我们来看看是否能真的带来性能的提升。修改上面的run()
函数,每个进程创建一个数组,然后计算数组中值的累加,创建数组的意图是保证进程用到了CPU核心的L1、L2缓存:
void run(int c, int n) {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(n, &mask);
sched_setaffinity(0, sizeof(cpu_set_t), &mask);
struct timeval tv;
gettimeofday(&tv, NULL);
long begin = tv.tv_sec * 1000 + tv.tv_usec / 1000;
int i;
int arr[N];
for (i = 0; i != N; i++) {
arr[i] = i;
}
long sum = 0;
for (i = 0; i != N; i++) {
sum += arr[i];
}
gettimeofday(&tv, NULL);
long end = tv.tv_sec * 1000 + tv.tv_usec / 1000;
printf("%ld\n", end - begin);
}
然后执行20次程序,10次没有CPU绑定,10次有CPU绑定,记录每个进程的耗时毫秒数,就有下面的结果:
P1~P20是进程号,A1~A10列是没有CPU绑定的情况,B1~B10列是有CPU绑定的情况,耗时越久单元格越红。可见绑定了CPU的情况下性能有近10%的提升。