前言
现在计算机上的CPU大多都是多核的,有4核甚至是8核的。但是一个计算机启动之后其进程数是远远多于CPU核数的,因为操作系统会给自动调度这些进程在CPU核上轮流运行。但是对于应用程序或者进程,其性能要求较高时,可能有必要绑定该进程到指定的CPU核来运行,避免调度带来的额外开销。我自己也是因为最近的项目上有需要进程运行在指定的CPU核上的要求,所以了解了一下这项技术,并且将过程和总结记录于此。
CPU亲和性
在学习这项新技术之前,我们先来了解一下什么是CPU亲和性?所谓亲和性,就是把进程在指定的 CPU 上尽量长时间地运行而不被迁移到其他处理器,也称为CPU关联性;再简单的点的描述就将制定的进程或线程绑定到相应的cpu上;在多核运行的机器上,每个CPU本身自己会有缓存,缓存着进程使用的信息,而进程可能会被OS调度到其他CPU上,如此,CPU cache命中率就低了,当绑定CPU后,程序就会一直在指定的cpu跑,不会由操作系统调度到其他CPU上,性能有一定的提高。
预备知识
在编写测试程序之前,我们先来了解一下CPU相关的宏和函数。
1.首先要想使用CPU系列函数及相关的宏,需要声明下面的宏,以告诉编译器启用这些函数
#define _GNU_SOURCE
2.声明一个cpu_set_t,然后用 CPU_ZERO()宏来初始化数据:
cpu_set_t mask;
CPU_ZERO(&mask);
3.再下来就是要指定绑定的CPU核心,用CPU_SET()宏来指定,例如:
CPU_SET(1, &mask);//绑定CPU核心1
4.下面这个函数是进程绑定CPU核心最关键函数,也是实际绑定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上运行。
用法:
sched_setaffinity(0, sizeof(cpu_set_t), &mask);//设置完mask之后,通过该函数完成实际绑定功能
5.补充:通过上面的4步就可以完成进程绑定CPU核心了,即被绑定的进程会一直运行在指定的CPU核上,不会被操作系统调度走。在这里,我们再介绍一下另外两个宏:
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 的一员,如果在CPU集中这个宏就返回一个非零值(true),否则就返回零(false)。
还有一个函数其功能是获取当前进程运行在哪一个CPU核:
int sched_getcpu();
测试程序
接下来,我们就用一个测试程序来看一下进程是否被绑定在一个CPU核上。
#define _GNU_SOURCE
#include
#include
#include
#include
int main()
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(1, &mask);//把main进程绑定到CPU1
sched_setaffinity(0, sizeof(cpu_set_t), &mask);//实际的绑定函数
printf("cpu = %d\n",sched_getcpu());//打印当前运行的CPU核,看是否绑定成功
int i,j,k;
for(i = 0;i < 40;i++)
{//循环40亿次
printf("i = %d\n",i);
for(j = 0;j < 10000;j++)
{
for(k = 0;k < 10000;k++)
{
if(sched_getcpu() != 1)//如果当前CPU核不是1,说明被操作系统给调度走了,绑定失败!并且打印当前运行的CPU核
{
printf("main process: cpu = %d\n",sched_getcpu());
}
}
}
}
printf("main end\n");
return 0;
}
编译测试程序:g++ cpu.cpp -o cpu
运行程序:./cpu
结果如下:
可以看到,打印的CPU核正是我们绑定的CPU,并且没有打印程序中测试绑定失败的相关log,说明循环了40亿次,main进程一直都在CPU1上运行的。
进阶测试
上面的测试,我们只是做到了被绑定的进程确实运行在了指定的CPU上,但是期间是否有其他进程被操作系统调度进来运行呢?被绑定的进程是否独享指定的CPU呢?现在我们就通过测试程序来测试看看。这里我们还是需要上面刚写的测试程序来辅助测试,这里暂且称上面的程序为测试程序1吧。下面我们要写第二个测试程序,其功能就是fork5个子进程,并且对这5个子进程不做绑定,任由操作系统默认调度,测试程序2如下:
#define _GNU_SOURCE
#include
#include
#include
#include
void run(int c, int n,int cpu) {
int lastcpu = cpu;
// if(c == 0)
// {
// cpu_set_t mask;
// CPU_ZERO(&mask);
// CPU_SET(n, &mask);
// sched_setaffinity(0, sizeof(cpu_set_t), &mask);
// printf("c= %d,cpu = %d\n",c,sched_getcpu());
// }
int i,j,k;
for (i = 0; i != 60; i++) //循环60亿次
{
printf("c = %d,i = %d\n",c,i);
for(j = 0; j != 10000; j++)
{
for(k = 0;k != 10000;k++)
{
if(sched_getcpu() != lastcpu)//如果当前的CPU核和上次的CPU核不一样,说明该进程被调度了,这样只要进程发生了调度我们就可以知道该进程是从CPU的哪个核调度到哪个核
{
printf("c = %d,i = %d,lastcpu = %d, nowcpu = %d\n",c,i,lastcpu,sched_getcpu());
lastcpu = sched_getcpu(); //更新lastcpu的值
}
}
}
}
printf("c = %d ,i = %d, j = %d,is end\n",c,i,j);
}
int main()
{
int i,initcpu;
for (i = 0; i != 5; i++) {//循环5次,产生了5个子进程
int pid = fork();
if (pid == 0) {//fork返回值为0表明是子进程,if判断里面只有子进程会跑
initcpu = sched_getcpu();//获取各个进程被分配到哪个CPU核
printf("i = %d,initcpu = %d\n",i,initcpu);
run(i, i,initcpu);//每一个子进程都会跑run函数
exit(0);
}
}
return 0;
}
好了,现在我们有一个绑定了CPU1的测试程序1,以及任由操作系统调度的测试程序2.
测试方法就是先执行测试程序1,目的是让测试程序1绑定CPU1,然后再运行测试程序2,由于测试程序2中的子进程都是操作系统默认调度的,所以可以看看测试程序1还在运行期间,测试程序2中的进程是否会被调度到CPU1上。
执行结果:
由于运行结果log较多,这里不一一上传,但是我们从上面的结果中可以看到,在测试程序1执行期间,测试程序2中的这些被系统自动调度的进程并没有一个进程被调度到CPU1上,直到程序1执行完了之后测试程序2的进程才被调度到CPU1上。
线程绑定CPU
线程绑定CPU的函数如下:
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset);
绑定线程与进程的过程和方法都类似,这里就不详细介绍了,读者可以在了解了进程绑定之后再亲自尝试一下线程绑定。
总结
经过上面测试程序的测试我们可以得出以下结论:
1.经过sched_setaffinity函数绑定了CPU之后,进程在其运行期间是独享该核心的,其他被操作系统自动调度的进程不会在该CPU核心上运行。
2.只有 在被绑定的进程运行结束之后,该核心就会被释放,供操作系统自动调度。