前言

随着对C++线程的研究和项目的推进,接触到CPU亲和性,即CPU绑定相关的内容,本文针对相关知识基础内容做一个介绍。


提示:以下是本篇文章正文内容,下面案例可供参考
在这里插入图片描述

一、CPU亲和性

1 前言

CPU的亲和性,进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性,进程迁移的频率小就意味着产生的负载小。当系统将线程分配给处理器时,操作系统使用亲和性来进行操作。这意味着如果所有其他因素相同的话,它将设法在它上次运行的那个处理器上运行线程。让线程留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。

亲和性一词是从affinity翻译来的,实际可以称为CPU绑定。

2 为何要手动绑定线程/进程到CPU核

大数据量的数据处理过程中,我们通常需要提升处理效率和性能,一是优化算法,二是充分利用服务器的硬件资源,如CPU和GPU。

平时应用程序在运行时都是由操作系统管理的。操作系统对应用进程进行调度,使其在不同的核上轮番运行。对于普通的应用程序,操作系统的默认调度机制是没有问题的。但是,当某个进程需要较高的运行效率时,就有必要考虑将其绑定到单独的核上运行,以减小由于在不同的核上调度造成的开销。

把某个进程/线程绑定到特定的cpu核上后,该进程就会一直在此核上运行,不会再被操作系统调度到其他核上。但绑定的这个核上还是可能会被调度运行其他应用程序的。

目前windows和linux都支持对多核cpu进行调度管理。软件开发在多核环境下的核心是多线程开发。这个多线程不仅代表了软件实现上多线程,要求在硬件上也采用多线程技术。

3 多进程和多线程在多核CPU上运行:

在多核运行的机器上,每个CPU本身自己会有缓存,在缓存中存着进程使用的数据,而没有绑定CPU的话,进程可能会被操作系统调度到其他CPU上,如此CPU cache(高速缓冲存储器)命中率就低了,也就是说调到的CPU缓存区中原来没有这类数据,要先把内存或硬盘的数据载入缓存。而当缓存区绑定CPU后,程序就会一直在指定的CPU执行,不会被操作系统调度到其他CPU,性能上会有一定的提高。

另外一种使用CPU绑定考虑的是将关键的进程隔离开,对于部分实时进程调度优先级提高,可以将其绑定到一个指定CPU核上,可以保证实时进程的调度,也可以避免其他CPU上进程被该实时进程干扰。我们可以手动地为其分配CPU核,而不会过多的占用同一个CPU,所以设置CPU亲和性可以使某些程序提高性能。

写到这里,联想到nginx有一项核心配置参数:worker_cpu_affinity:将进程与CPU绑定,提高了CPU cache的命中率,从而减少内存访问损耗,提高程序的速度。

4 应用场景举例

将UI线程限制在一个CPU,将其他实时性要求较高的线程限制在另一个CPU。这样,当UI需要占用大量CPU时间时,就不会拖累其他实时性要求较高的线程的执行。同样可以将UI线程与一些优先级不高但耗时的异步运算线程设置在不同CPU上,避免UI给人卡顿的感觉。

二、Linux的CPU亲和性特征

Linux的调度程序同时提供”软CPU亲和性”和”硬CPU亲和性”。

1 软亲和性

进程要在指定的 CPU 上尽量长时间地运行而不被迁移到其他CPU。

Linux 内核进程调度器天生就具有被称为软CPU 亲和性(affinity) 的特性,因此linux通过这种软的亲和性试图使某进程尽可能在同一个CPU上运行。

2 硬亲和性

将进程或者线程绑定到某一个指定的cpu核运行。

虽然Linux尽力通过一种软的亲和性试图使进程尽量在同一个处理器上运行,但它也允许用户强制指定进程无论如何都必须在指定的处理器上运行。

3 硬亲和性使用场景

需要保持高CPU缓存命中率时、需要测试复杂的应用程序时。

保持高CPU缓存命中率:如果一个给定的进程迁移到其他地方去了,那么它就失去了利用 CPU 缓存的优势。实际上,如果正在使用的 CPU 需要为自己缓存一些特殊的数据,那么所有其他 CPU 都会使这些数据在自己的缓存中失效。因此,如果有多个线程都需要相同的数据,那么将这些线程绑定到一个特定的 CPU 上是非常有意义的,这样就确保它们可以访问相同的缓存数据(或者至少可以提高缓存的命中率)。否则,这些线程可能会在不同的 CPU 上执行,这样会频繁地使其他缓存项失效。

测试复杂的应用程序:考虑一个需要进行线性可伸缩性测试的应用程序。有些产品声明可以在使用更多硬件时执行得更好。 我们不用购买多台机器(为每种处理器配置都购买一台机器),而是可以:

  1. 购买一台多处理器的机器;
  2. 不断增加分配的处理器;
  3. 测量每秒的事务数;
  4. QQ啊·评估结果的可伸缩性。

三、查看CPU的核

1 使用指令

使用cat /proc/cpuinfo查看cpu信息,如下两个信息:

  • processor,指明第几个cpu处理器
  • cpu cores,指明每个处理器的核心数

总核数 = 物理CPU个数 X 每颗物理CPU的核数
总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数
查看物理CPU个数
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
查看每个物理CPU中core的个数(即核数)
cat /proc/cpuinfo| grep "cpu cores"| uniq
查看逻辑CPU的个数
cat /proc/cpuinfo| grep "processor"| wc -l

物理CPU:主板上安装的CPU个数。

逻辑CPU:一般情况,我们认为一颗CPU可以有多个核,加上intel的超线程技术(HT), 可以在逻辑上再分一倍数量的CPU core出来。

超线程技术(Hyper-Threading):就是利用特殊的硬件指令,把单个物理CPU模拟成两个CPU(逻辑CPU),实现多线程。我们常听到的双核四线程/四核八线程指的就是支持超线程技术的CPU。

2 使用sysconf

使用系统调用sysconf获取cpu核心数:

#include <unistd.h>
// 返回系统可以使用的核数,但是其值会包括系统中禁用的核的数目,因此该值并不代表当前系统中可用的核数
int sysconf(_SC_NPROCESSORS_CONF);
// 返回值真正的代表了系统当前可用的核数
int sysconf(_SC_NPROCESSORS_ONLN);
 
/* 以下两个函数与上述类似 */
#include <sys/sysinfo.h>
int get_nprocs_conf (void);/* 可用核数 */
int get_nprocs (void);/* 真正的反映了当前可用核数 */

四、Linux操作系统中修改CPU亲和性的方法

1 taskset

1.1 获取进程pid:ps

% ps
PID TTY TIME CMD
2683 pts/1 00:00:00 zsh
2726 pts/1 00:00:00 dgram_servr
2930 pts/1 00:00:00 ps

1.2 查看进程当前运行在哪个cpu上

% taskset -p 2726
pid 2726’s current affinity mask: 3

显示的十进制数字3转换为2进制为11,每个1对应一个cpu,所以进程运行在2个cpu上。

可通过 taskset -pc $pid 来获取某线程与CPU核心的亲和性(线程在运行中可能执行于CPU核心的集合)。

1.3 指定进程运行在cpu1上

taskset将一个启动的进程直接绑定在某个核上运行:

# cpu-list可以是0,1这样的一个核,也可以是1-2这样的多个核,表示绑定在1和2上面
# pid 表示进程号
taskset -cp cpu-list pid

例如说

taskset -cp 1-3 1927

这句命令就是表示将进程号为1927的进程绑定在核1,2,3上。

% taskset -pc 1 2726
pid 2726’s c urrent affinity list: 0,1
pid 2726’s new affinity list: 1

注意,cpu的标号是从0开始的,所以cpu1表示第二个cpu(第一个cpu的标号是0)。

至此,就把应用程序绑定到了cpu1上运行,查看如下:

-> % taskset -p 2726

pid 2726's current affinity mask: 2

2的二进制是10,所以代表是CPU1,CPU0是01

1.4 如何确认绑定成功

这时候就可以通过top命令来进行查看,首先执行top命令,就可以看到相应的资源消耗,然后按F,选择Last Used CPU,这时候可以使用空格选中:

然后再按ECS,就可以退回正常Top界面,这时候就可以看到这些进程跑在哪些核上,如果说Last Used CPU一直保持不变,那就说明绑定成功。

1.5 启动程序时绑定cpu

#启动时绑定到第二个cpu
-> % taskset -c 1 ./dgram_servr&
[1] 3011

#查看确认绑定情况
-> % taskset -p 3011
pid 3011's current affinity mask: 2

使用sched_setaffinity系统调用

sched_setaffinity可以将某个进程绑定到一个特定的CPU。

见下面Linux API部分讲解。

2 Linux API

在Linux内核中,所有的进程都有一个相关的数据结构,称为 task_struct。这个结构非常重要,其中与亲和性(affinity)相关度最高的是 cpus_allowed 位掩码。这个位掩码由 n 位组成,与系统中的 n 个逻辑处理器对应。 具有 4 个物理 CPU 的系统可以有 4 位。如果这些 CPU 都启用了超线程,那么这个系统就有8个位掩码。 如果为给定的进程设置了给定的位,那么这个进程就可以在相关的 CPU 上运行。因此,如果一个进程可以在任何 CPU 上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是 1。这是 Linux 中进程的预设状态。

为了让程序拥有更好的性能,有时候需要将进程或线程绑定到特定的CPU上,这样可以减少调度的开销和保护关键进程或线程。

2.1 绑定进程到指定的CPU

Linux提供一个接口,可以将进程绑定到特定的CPU:

#include <sched.h>
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *set);
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *set);

设置进程号为pid的进程运行在set所设定的CPU上
参数:

  • pid:进程的id号,如果pid为0,则表示本进程
  • cpusetsize:set所指定的数的长度,通常设定为sizeof(cpu_set_t)
  • set:运行进程的CPU,可以通过以下函数操作set:
void CPU_ZERO(cpu_set_t *set); // Clears set, so that it contains no CPUs.
void CPU_SET(int cpu, cpu_set_t *set); // Add CPU cpu to set.
void CPU_CLR(int cpu, cpu_set_t *set); // Remove CPU cpu from set.
int  CPU_ISSET(int cpu, cpu_set_t *set); // Test to see if CPU cpu is a member of set.
int  CPU_COUNT(cpu_set_t * mask); //Return the number of CPUs in set.

进程绑定到指定CPU的示例程序如下:

2.1.1 示例1
/*
*gcc process_test.c
*/

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <math.h>
#include <sched.h>

void WasteTime(void)
{
    int abc = 1000;
	int tmp = 0;
    while(abc--)
        tmp = 10000*10000;
    	sleep(1);
}

int main(int argc, char *argv[])
{
    cpu_set_t cpu_set;
    while(1)
    {
        CPU_ZERO(&cpu_set);
        CPU_SET(0, &cpu_set);
        if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) < 0)
            perror("sched_setaffinity");
        WasteTime();

        CPU_ZERO(&cpu_set);
        CPU_SET(1, &cpu_set);
        if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) < 0)
            perror("sched_setaffinity");
        WasteTime();
        CPU_ZERO(&cpu_set);
        CPU_SET(2, &cpu_set);
        if (sched_setaffinity(0, sizeof(cpu_set), &cpu_set) < 0)
            perror("sched_setaffinity");
        WasteTime();


        CPU_ZERO(&cpu_set);
        CPU_SET(3, &cpu_set);
        if(sched_setaffinity(0, sizeof(cpu_set), &cpu_set) < 0)
            perror("sched_setaffinity");
        WasteTime();
    }
    return 0;
}

测试:编译程序,之后运行,我编译出的文件名为out,执行下列命令,得到out的PID:ps -elf | grep out,之后输入命令:top -p 进程ID,接着输入f,选择P选项(移到P处,按下空格),按ESC退出,此时可以看到进程在cpu0 cpu1 cpu2 cpu3之间不停切换。

2.1.2 示例2
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/sysinfo.h>
#include<unistd.h>

#define __USE_GNU
#include<sched.h>
#include<ctype.h>
#include<string.h>
#include<pthread.h>
#define THREAD_MAX_NUM 200  //1个CPU内的最多进程数

int num=0;  //cpu中核数
void* threadFun(void* arg)  //arg  传递线程标号(自己定义)
{
	cpu_set_t mask;  //CPU核的集合
	cpu_set_t get;   //获取在集合中的CPU
	int *a = (int *)arg; 
	int i;
	
	printf("the thread is:%d\n",*a);  //显示是第几个线程
	CPU_ZERO(&mask);    //置空
	CPU_SET(*a,&mask);   //设置亲和力值
	if (sched_setaffinity(0, sizeof(mask), &mask) == -1)//设置当前进程CPU亲和力
	{
	          printf("warning: could not set CPU affinity, continuing...\n");
	}

	CPU_ZERO(&get);
	if (sched_getaffinity(0, sizeof(get), &get) == -1)//获取线程CPU亲和力
	{
	        printf("warning: cound not get thread affinity, continuing...\n");
	}
	for (i = 0; i < num; i++)
	{
		if (CPU_ISSET(i, &get))//判断线程与哪个CPU有亲和力
		{
	        printf("this thread %d is running processor : %d\n", *a,i);
		}
	}
	return NULL;
}

int main(int argc, char* argv[])
{
	int tid[THREAD_MAX_NUM];
	int i;
	pthread_t thread[THREAD_MAX_NUM];
	
	num = sysconf(_SC_NPROCESSORS_CONF);  //获取核数
	if (num > THREAD_MAX_NUM) {
	   printf("num of cores[%d] is bigger than THREAD_MAX_NUM[%d]!\n", num, THREAD_MAX_NUM);
	   return -1;
	}
	printf("system has %i processor(s). \n", num);
	
	for(i=0;i<num;i++)
	{
        tid[i] = i;  //每个线程必须有个tid[i]
        pthread_create(&thread[i],NULL,threadFun,(void*)&tid[i]);
	}
	for(i=0; i< num; i++)
	{
        pthread_join(thread[i],NULL);//等待所有的线程结束,线程为死循环所以CTRL+C结束
	}
	return 0;                                                                    
}

结果

% ./a.out
% system has 2 processor(s). 
% the thread is:0
% the thread is:1
% this thread 0 is running processor : 0
% this thread 1 is running processor : 1

2.2 绑定线程到指定的CPU

不仅仅进程可以绑定到CPU,线程也可以。Linux提供一个接口,可以将线程绑定到特定的CPU:

#include <pthread.h>
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);

该接口与进程绑定到CPU的接口的使用方法基本一致。

当进程绑定到特定的CPU之后,线程还是可以绑定到其他的CPU的,没有冲突。

2.2.1 示例1
/*
*gcc thread_test.c -lpthread
*/
 
//#define _GNU_SOURCE
#include <stdio.h>
#include <math.h>
#include <pthread.h>
#include <unistd.h>
#include <sched.h>
 
void WasteTime(void)
{
    int abc = 1000;
    int temp = 0;
 
    while(abc--)
        temp = 10000*10000;
 
    sleep(1);
}
 
void *thread_func1(void *param)
{
    cpu_set_t cpu_set;
    while(1)
    {
        CPU_ZERO(&cpu_set);
        CPU_SET(1, &cpu_set);
 
        if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set),&cpu_set) < 0)
            perror("pthread_setaffinity_np");
        WasteTime();
 
        CPU_ZERO(&cpu_set);
        CPU_SET(2, &cpu_set);
 
        if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set),&cpu_set) < 0)
            perror("pthread_setaffinity_np");
        WasteTime();
    }
}
 
void *thread_func2(void *param)
{
    cpu_set_t cpu_set;
    while(1)
    {
        CPU_ZERO(&cpu_set);
        CPU_SET(3, &cpu_set);
 
        if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set),&cpu_set) < 0)
            perror("pthread_setaffinity_np");
        WasteTime();
    }
}
 
int main(int argc, char *argv[])
{
    pthread_t my_thread;
    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(0, &cpu_set);
    if (sched_setaffinity(0, sizeof(cpu_set), &cpu_set) < 0)
        perror("sched_setaffinity");
 
    if (pthread_create(&my_thread, NULL, thread_func1,NULL) != 0)
        perror("pthread_create");
    if (pthread_create(&my_thread, NULL, thread_func2,NULL) != 0)
        perror("pthread_create");
 
    while(1)
        WasteTime();
 
    pthread_exit(NULL);
}

测试:编译程序,之后运行。编译出的文件名为a.out,执行ps -elf | grep a.out,得到a.out的PID,之后输入命令:top -H -p? 进程ID,接着输入f,选择P选项(移到P处,按下空格)和nTH选项,按ESC退出,可以看到主线程一直保持在cpu0,一个线程在cpu1 cpu2之间切换,另一个线程一直保持在cpu3。

2.2.2 示例2
#define _GNU_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define handle_error_en(en, msg) \
        do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char *argv[])
{
    int s, j;
    cpu_set_t cpuset;
    pthread_t thread;

    thread = pthread_self();

    /* Set affinity mask to include CPUs 0 to 7 */

    CPU_ZERO(&cpuset);
    for (j = 0; j < 8; j++)
        CPU_SET(j, &cpuset);

    s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (s != 0)
        handle_error_en(s, "pthread_setaffinity_np");

    /* Check the actual affinity mask assigned to the thread */

    s = pthread_getaffinity_np(thread, sizeof(cpu_set_t), &cpuset);
    if (s != 0)
        handle_error_en(s, "pthread_getaffinity_np");

    printf("Set returned by pthread_getaffinity_np() contained:\n");
    for (j = 0; j < CPU_SETSIZE; j++)
        if (CPU_ISSET(j, &cpuset))
            printf("    CPU %d\n", j);

    exit(EXIT_SUCCESS);
}

运行结果:

如果只有两个processor,那么输出:

-> % ./a.out 
Set returned by pthread_getaffinity_np() contained:
    CPU 0
    CPU 1

五、总结

本文简单介绍了什么是CPU的亲和性,以及为什么要手动绑定CPU核和使用此方案的场景。接着介绍了查看本机CPU核数量的指令和方法,然后主要介绍Linux操作系统中编程实现绑定进程和线程到指定的CPU核的方法和示例程序,最后附加简单介绍了windows平台上设置处理器亲和性的API接口。希望本文能给大家带来一些帮助,资料整理不易,共同学习。另,如有侵权,私信联系我。

六、附加:Windows平台

使用函数SetProcessAffinityMask ,头文件#include<winbase.h>

Sets a processor affinity mask for the threads of the specified process.

Syntax

BOOL SetProcessAffinityMask(
  HANDLE    hProcess,
  DWORD_PTR dwProcessAffinityMask
);

参数一:进程句柄 -1为自身句柄

参数二:指定CPU

参数二的设置是二进制转十进制,需填写十进制数字

例如我想设置

1,则CPU二进制为1,转换为十进制为 1

2,则CPU二进制为10,转换为十进制为 2

七、参考

[1] Linux:查看线程运行于哪个CPU核心上
[2] SetProcessAffinityMask function (winbase.h)
[3] Linux绑定CPU运行指定进程(绑核)-taskset
[4] linux下把进程/线程绑定到特定cpu核上运行

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐