C/C++线程绑核详解

        在一些大型的工程或者特殊场景中,我们会听到绑核,绑核分为进程绑核和线程绑核。绑核的最终目的都是为了提高程序和性能或者可靠性。

一:为什么需要绑核

        操作系统发展至今,已经能很好的平衡运行在操作系统上层的应用,兼顾性能和可靠性。一般情况下,在应用程序中只需使用缺省的调度器即可。然而,在某些场景下操作系统默认的调度算法会造成程序的瓶颈,要突破这个瓶颈可能要修改这些缺省行为以实现性能上的突破。以下三个场景比较适合去绑核操作。

1,大量的计算

        基于大量计算的情形通常出现在科学和理论计算中,但是通用领域的计算也可能出现这种情况。一个常见的标志是您发现自己的应用程序要在多处理器的机器上花费大量的计算时间。

 2,测试复杂的应用程序

        测试复杂软件是我们对内核的亲和性(affinity)技术感兴趣的另外一个原因。考虑一个需要进行线性可伸缩性测试的应用程序。有些产品声明可以在 使用更多硬件 时执行得更好。

3,正在运行时间敏感的、决定性的进程 

        我们对 CPU 亲和性(affinity)感兴趣的最后一个原因是实时(对时间敏感的)进程。例如,您可能会希望使用硬亲和性(affinity)来指定一个 8 路主机上的某个处理器,而同时允许其他 7 个处理器处理所有普通的系统调度。这种做法确保长时间运行、对时间敏感的应用程序可以得到运行,同时可以允许其他应用程序独占其余的计算资源。

        在DPDK中,由于需要频繁的去收发包,为了提高收发包的性能,绑核操作在DPDK中非常的常见。DPDK通过把线程绑定到逻辑核的方法来避免跨核任务中的切换开销,减少线程调度的开销,来提高性能。 

二:绑核的基本概念

1,CPU的亲和性

        在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。Linux 内核进程调度器天生就具有被称为 软 CPU 亲和性(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。

        上面提到了软亲和性,那对应的就有硬亲和性。soft affinity和hard affinity。soft affinity仅是一个建议,如果不可避免,调度器还是会把进程调度到其它的CPU上。hard affinity是调度器必须遵守的规则。

2,CPU亲和性掩码

        在绑核的操作中会经常看到mask这个单词,可能初学的不太理解这个mask代表的意义。学过TCP/IP的应该知道mask在ip中就是掩码的意思,利用掩码来划分子网,本质上就是用1和对应位上做&操作。

        在 Linux 内核中,所有的进程都有一个相关的数据结构,称为 task_struct。这个结构非常重要,原因有很多;其中与 亲和性(affinity)相关度最高的是 cpus_allowed 位掩码。这个位掩码由 n 位组成,与系统中的 n 个逻辑处理器一一对应。 具有 4 个物理 CPU 的系统可以有 4 位。如果这些 CPU 都启用了超线程,那么这个系统就有一个 8 位的位掩码。

        如果为给定的进程设置了给定的位,那么这个进程就可以在相关的 CPU 上运行。因此,如果一个进程可以在任何 CPU 上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是 1。实际上,这就是 Linux 中进程的缺省状态。

        我们可以查看自己服务器有多少个核心

[ftz@host6 ~]$ cat /proc/cpuinfo |grep processor | wc -l
64
[ftz@host6 ~]$ nproc
64

 3,绑核的代码接口

 以下是绑核的常用宏接口:

//结构体cpu_set_t,定义了CPU核心集合的数据结构
cpu_set_t *set

//初始化cpu set集合
void CPU_ZERO (cpu_set_t *set); 

//单个cpu加到集合中
void CPU_SET (int cpu, cpu_set_t *set);

//单个cpu从集中移出
void CPU_CLR (int cpu, cpu_set_t *set); 

//判断某个cpu是否在cpu集中
int CPU_ISSET (int cpu, const cpu_set_t *set); 

        cpu集可以认为是一个掩码,每个设置的位都对应一个可以合法调度的 cpu,而未设置的位则对应一个不可调度的 CPU。换而言之,线程都被绑定了,只能在那些对应位被设置了的处理器上运行。通常,掩码中的所有位都被置位了,也就是可以在所有的cpu中调度。

 线程绑核用到的函数接口:

long sched_setaffinity(pid_t pid, const struct cpumask *in_mask)

        该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上.如果pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上.第二个参数cpusetsize是mask所指定的数的长度.通常设定为sizeof(cpu_set_t).如果当前pid所指定的进程此时没有运行在mask所指定的任意一个CPU上,则该指定的进程会从其它CPU上迁移到mask的指定的一个CPU上运行. 

long sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask) 

         该函数获得pid所指示的进程的CPU位掩码,并将该掩码返回到mask所指向的结构中.即获得指定pid当前可以运行在哪些CPU上.同样,如果pid的值为0.也表示的是当前进程

三:C语言代码实现线程绑核

 代码实现:起三个线程绑到三个cpu上

#define _GNU_SOURCE  //该宏定义必须有
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sched.h>
#include <unistd.h>
#include <pthread.h>

#define CREATE_THREAD_NUMBER 3
#define JUDGE_IS_SUCCESS(RET) (RET == -1)

cpu_set_t g_cpuset;  //定义cpu集合

struct thread_info
{
    pthread_t thread_id;
    int thread_num;
};

void do_thing_busy_cpu()
{
    int j = 0;
    int buf[10240];
    while(j++ < 1024*1024)
    {
        memset(buf, 0, sizeof(buf));
    }
}

void *threadFunc(void *arg)
{
    struct thread_info *subThreadInfo = (struct thread_info *)arg;
    pthread_t subThreadID = subThreadInfo->thread_id;

    
    CPU_SET(subThreadInfo->thread_num, &g_cpuset); //将编号为subThreadInfo->thread_num的cpu核加到cpuset集合中

    if(JUDGE_IS_SUCCESS(sched_setaffinity(getpid(),sizeof(cpu_set_t),&g_cpuset)))
    {
        printf("ERROR:set CPU affinity fail!\n");
    }
    
    while(1)
    {
        cpu_set_t getCpuset;
        CPU_ZERO(&getCpuset);
        if(JUDGE_IS_SUCCESS(sched_getaffinity(getpid(),sizeof(cpu_set_t),&getCpuset)))
        {
            printf("ERROR:get CPU affinity fail!\n");
        }
        for(int i=0; i<CREATE_THREAD_NUMBER; i++)
        {
            if(CPU_ISSET(i,&getCpuset))
            {
                printf("thread %d is running on cpu: %d\n",subThreadInfo->thread_id,i);
                do_thing_busy_cpu();
            }
        }        
    }
    
    return NULL;
}

int main(int argc, char *argv[])
{
    struct thread_info threadInfo[CREATE_THREAD_NUMBER];
    int cpuNum = sysconf(_SC_NPROCESSORS_CONF);
    printf("cpu number:%d\n",cpuNum);


    CPU_ZERO(&g_cpuset); //初始化cpu集合

    //创建3个线程
    for(int i=0; i<CREATE_THREAD_NUMBER; i++)
    {
        threadInfo[i].thread_num = i;
        pthread_create(&threadInfo[i].thread_id,NULL,threadFunc,&threadInfo[i]);
    }

    for(int i=0; i<CREATE_THREAD_NUMBER; i++)
    {
        pthread_join(threadInfo[i].thread_id,NULL);
    }
}

编译:

gcc bindcpu.c -o bindcpu -lpthread -std=c99

运行结果:

cpu number:64
thread 1235138304 is running on cpu: 0
thread 1226745600 is running on cpu: 0
thread 1218352896 is running on cpu: 0
thread 1235138304 is running on cpu: 0
thread 1226745600 is running on cpu: 1
thread 1218352896 is running on cpu: 1
thread 1235138304 is running on cpu: 1
thread 1226745600 is running on cpu: 0
thread 1218352896 is running on cpu: 2
thread 1235138304 is running on cpu: 2
thread 1226745600 is running on cpu: 1
thread 1218352896 is running on cpu: 0
thread 1235138304 is running on cpu: 0
thread 1226745600 is running on cpu: 2
thread 1218352896 is running on cpu: 1
thread 1235138304 is running on cpu: 1
thread 1226745600 is running on cpu: 0
thread 1218352896 is running on cpu: 2
thread 1235138304 is running on cpu: 2
thread 1226745600 is running on cpu: 1
...

可以用top命令查看进程是不是占用了三个cpu

 按1查看cpu核使用详情,由于我用的服务器是共享的大家都在上面用,所以直观的看不出来

 可以通过taskset命令来查看我们程序的CPU亲和度:

[ftz@host6 ~]$ taskset -p 1376833
pid 1376833's current affinity mask: 7

7的二进制就是111,正好符合我们的预期

 除了用代码绑核,也可以用shell命令taskset来设置进程的 CPU 亲和度 (affinity),它可以将一个进程绑定到某个特定的 CPU 核心或一组 CPU 核心。

1,绑定一个进程到单个 CPU 核心:
taskset -c <core_id> <command>
例:将进程绑定到第 1 个 CPU 核心上
taskset -c 0 command

2,绑定一个进程到多个 CPU 核心
taskset -c <core_id_list> <command>
例:进程绑定到第 1、2、4 个 CPU 核心上
taskset -c 0,1,3 command

3,显示一个进程当前的 CPU 亲和度:
taskset -p <pid>

4,修改一个正在运行的进程的 CPU 亲和度:
taskset -cp <core_id_list> <pid>
例:将进程 12345 绑定到第 1、2、4 个 CPU 核心上
taskset -cp 0,1,3 12345

猜你喜欢

转载自blog.csdn.net/qq_27071221/article/details/131021926