【linux C】绑定任务到特定CPU(CPU亲和性)

1 相关概念及工具

1.1 CPU亲和性

CPU亲和性(affinity)就是进程要在某个给定的CPU上尽量长时间的运行而不被迁移到其他处理器的倾向性。
linux内核进程调度器天生具有软CPU亲和性(affinity)的特性,着意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。
Linux内核还包含一些机制,它让开发人员可以编程实现硬CPU亲和性(affinity)。着意味着应用程序可以显示的指定进程在那个(或那些)处理器上运行。
引用自CPU亲和性

注:本文中所有的术语CPU,都要狭义的理解为一个单核CPU(更严格的说,本文所说的一CPU,都是最小执行单元,比如cpu0是一个执行单元,cpu1又是一个执行单元,每个执行单元上,都可以跑一个线程),不要考虑一个CPU又包含多少多少个核心的情况。
注:本文中的术语,在本文中,同CPU是一个概念

1.2 查看cpu有几个核

1.2.1 命令查看

$ cat /proc/cpuinfo |grep processor | wc -l
# 或
$ nproc

# 使用cat /proc/cpuinfo可以更详细的查看哪些可以用,哪些不可以用
# 如可以先禁用一个cpu1
$ echo 0 > /sys/devices/system/cpu/cpu1/online
$ cat /proc/cpuinfo
# 再解除禁用,再对比cat /proc/cpuinfo的输出
$ echo 1 > /sys/devices/system/cpu/cpu1/online
$ cat /proc/cpuinfo
# 输出已省略,可自行实验。可以看到,禁用的时候,是没有processor1的

1.2.2 代码查看

#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);/* 真正的反映了当前可用核数 */

1.3 taskset命令

#查看一个已存在的任务的亲和性
#注意,该命令显示的结果,是一个十六进制数。如显示32,实际上为0x32,表示亲和性为cpu1,cpu4,cpu5(从cpu0开始计数)
taskset -p <pid>
#执行一个shell命令并设置其亲和性
#注意,这里的21表示的是掩码,十六进制,即0x21,意为cpu0和cpu5
taskset 21 <cmd>

更多用法参见manpage

2 代码讲解

2.1 cpu集(cpu_set_t)

2.1.1 cpu_set_t的使用

cpu_set_t用来描述CPU的集合,被sched_setaffinity等类似的函数使用。

2.1.1.1 非动态分配cpu_set_t

常用接口声明(在glibc中用本质为宏定义):

       void CPU_ZERO(cpu_set_t *set);
       void CPU_SET(int cpu, cpu_set_t *set);
       void CPU_CLR(int cpu, cpu_set_t *set);
       int  CPU_ISSET(int cpu, cpu_set_t *set);
       int  CPU_COUNT(cpu_set_t *set);
       void CPU_AND(cpu_set_t *destset,
                    cpu_set_t *srcset1, cpu_set_t *srcset2);
       void CPU_OR(cpu_set_t *destset,
                    cpu_set_t *srcset1, cpu_set_t *srcset2);
       void CPU_XOR(cpu_set_t *destset,
                    cpu_set_t *srcset1, cpu_set_t *srcset2);
       int  CPU_EQUAL(cpu_set_t *set1, cpu_set_t *set2);

用法举例:

cpu_set_t set1, set2;
CPU_ZERO(&set1); //清空集合,即set1里不包含任何CPU,本质为所有bit清零
CPU_ZERO(&set2); //清空集合,即set2里不包含任何CPU,本质为所有bit清零
CPU_SET(0, &set1); //将cpu0添加到集合set1中,本质为对应bit置1
CPU_SET(1, &set2); //将cpu1添加到集合set2中,本质为对应bit置1
CPU_CLR(0, &set1); //将cpu0从集合set1中移除,本质为对应bit清零
int ret = CPU_ISSET(1, &set2); //判断cpu1是否在集合set2中,在返回非零,不在返回0
int cnt = CPU_COUNT(&set2); //返回集合set2中的CPU的个数
cpu_set_t result;
CPU_AND(&result, &set1, &set2); //set1和set2的所有bit按位与,结果存入result
CPU_OR(); //按位或
CPU_XOR(); //按位异或
ret = CPU_EQUAL(&set1, &set2); //集合set1和集合set2相等的话,ret为非零,不相等,ret为0
2.1.1.2 动态分配cpu_set_t

刚开始接触cpu_set_t时,对_S系列接口有疑问,不明白它存在的意义,明明自己malloc一个cpu_set_t就可以,然后使用各种非_S对其操作,为什么非要有_S系列接口呢?具体原因见glibc源码粗讲解
常用接口声明(在glibc中用本质为宏定义):

       cpu_set_t *CPU_ALLOC(int num_cpus);
       void CPU_FREE(cpu_set_t *set);
       size_t CPU_ALLOC_SIZE(int num_cpus);
       void CPU_ZERO_S(size_t setsize, cpu_set_t *set);
       void CPU_SET_S(int cpu, size_t setsize, cpu_set_t *set);
       void CPU_CLR_S(int cpu, size_t setsize, cpu_set_t *set);
       int  CPU_ISSET_S(int cpu, size_t setsize, cpu_set_t *set);
       int  CPU_COUNT_S(size_t setsize, cpu_set_t *set);
       void CPU_AND_S(size_t setsize, cpu_set_t *destset,
                    cpu_set_t *srcset1, cpu_set_t *srcset2);
       void CPU_OR_S(size_t setsize, cpu_set_t *destset,
                    cpu_set_t *srcset1, cpu_set_t *srcset2);
       void CPU_XOR_S(size_t setsize, cpu_set_t *destset,
                    cpu_set_t *srcset1, cpu_set_t *srcset2);
       int  CPU_EQUAL_S(size_t setsize, cpu_set_t *set1, cpu_set_t *set2);

用法举例:

#define _GNU_SOURCE
#include <sched.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <assert.h>

int
main(int argc, char *argv[])
{
    cpu_set_t *cpusetp;
    size_t size;
    int num_cpus, cpu;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <num-cpus>\n", argv[0]);
        exit(EXIT_FAILURE);
    }   

    num_cpus = atoi(argv[1]);
    
    /* 申请一个能够存放num_cpus个CPU的集合 */
    cpusetp = CPU_ALLOC(num_cpus);
    if (cpusetp == NULL) {
        perror("CPU_ALLOC");
        exit(EXIT_FAILURE);
    }   

    /* 获取能存放num_cpus个CPU的集合的大小(单位字节) */
    size = CPU_ALLOC_SIZE(num_cpus);

    /* 清零cpusetp */
    CPU_ZERO_S(size, cpusetp);
    for (cpu = 0; cpu < num_cpus; cpu += 2)
        CPU_SET_S(cpu, size, cpusetp); //将cpu0,cpu2,... 添加到cpusetp集合

    printf("CPU_COUNT() of set:    %d\n", CPU_COUNT_S(size, cpusetp));

    /* 释放cpusetp空间 */
    CPU_FREE(cpusetp);
    exit(EXIT_SUCCESS);
}

2.1.2 glibc源码粗讲解(无兴趣可跳过)

/* Data structure to describe CPU mask.  */
typedef struct
{
  /* 不同架构下,__cpu_mask的类型不同,但粗略查看源码,均为unsigned long */
  __cpu_mask __bits[__CPU_SETSIZE / __NCPUBITS];
} cpu_set_t;

__NCPUBITS:一个宏,定义为__cpu_mask类型的位宽。在64位操作系统下,unsigned long占8字节,即位宽为64bit。
__CPU_SETSIZE:固定数值,1024。该宏定义的逻辑意义是,定义了cpu_set_t这个类型,可以包含多少个CPU。即定义一个cpu_set_t cpus;变量的话,最多可以表示1024个CPU。
cpu_set_t实际就是一个__cpu_mask(无符号long类型)数组,并且数组所占的总位宽为1024。

1024个bit为128Byte,在x86_64架构下,unsigned long占8字节,所以,当定义一个cpu_set_t变量时,实际上是定义了一个包含16个unsigned long的数组,共占128字节。所有的操作(如CPU_ZEROCPU_SET等)都是通过与、或、移位等操作,对每个bit进行清零或置一。如,要把cpu0添加到集合,则把bit0置一;要把cpu70添加到集合,就把bit70置一(显然,bit70在数组的第2个元素中,即bit0~bit63是第一个unsigned long,bit64~bit127是第二个unsigned long,以此类推)。

当想使用动态分配内存的方式定义cpu_set_t时,是可以直接malloc(sizeof(cpu_set_t)),然后直接使用非_S系列的接口对其操作即可(使用_S系列也可,只是需要将sizeof(cpu_set_t)作为参数传入_S系列函数。非_S系列接口,实现的时候也是调用的_S系列接口,只是size参数统一传入sizeof(cpu_set_t))。

cpu_set_t *CPU_ALLOC(int num_cpus)函数说明:该函数本质也是调用malloc,只是其申请的内存大小,足够包含num_cpus个CPU就好了,不会一股脑直接申请128B(1024bit)空间。该函数申请空间的最小单位是sizeof(__cpu_mask),在x86_64下,该值为sizeof(unsigned long),即8B。

#define __CPU_ALLOC_SIZE(count) \
  ((((count) + __NCPUBITS - 1) / __NCPUBITS) * sizeof (__cpu_mask))
#define __CPU_ALLOC(count) __sched_cpualloc (count)

cpu_set_t *
__sched_cpualloc (size_t count)
{
  return malloc (CPU_ALLOC_SIZE (count));
}

一般来说,用非_S系列函数就够了,即便是想动态分配内存,非_S也是可以的。
总结_S应用场景:

  • 想省空间。一般来说,CPU个数不会很大(远小于1024个),那么使用非_S就意味着,cpu_set_t要占用128B内存空间。如果想省空间,可以使用CPU_ALLOC来申请更精准大小的空间。
  • 你的代码要操控的CPU个数超过1024。

2.2 绑定任务到指定CPU

CPU亲和性只是一种倾向性,当绑定的CPU不存在或者存在但是被禁用了,任务会在其他的CPU上执行
设置任务亲和性的接口有:

  • sched_setaffinity:修改指定pid_t的任务的亲和性
  • pthread_setaffinity_np:gnu接口,修改指定pthrad_t的任务的亲和性。
  • pthread_attr_setaffinity_np:gnu接口。创建线程前,通过线程属性结构体控制新线程的亲和性。

注:np的意思为不可移植,即在非gnu的系统上,应该是没有这个接口的。

2.2.1 sched_setaffinity

如果考虑可移植性的话,推荐使用sched_setaffinity()函数将任务绑定到特定CPU执行。

test.c:

/* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <sched.h>

void *func(void *arg)
{
  pthread_setname_np(pthread_self(), (char *)arg);
  while(1)
  {
    char name[128] = {};
    printf("%s\n", (char *)arg);
    /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
    for(int i=0; i<1000000000; i++);
  }
  return NULL;
}

int main(void)
{
  pthread_t th1, th2;
  cpu_set_t set;

  /* 初始化一个CPU集合,并向其中添加cpu0和cpu3 */
  CPU_ZERO(&set);
  CPU_SET(0, &set);
  CPU_SET(3, &set);

  /* 设置主线程的亲和性,子线程会继承父线程的亲和性设置(见man pthread_create) */
  int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
  if (ret)
  {
    perror("sched_setaffinity");
    return -1;
  }

  pthread_create(&th1, NULL, func, "th1");
  pthread_create(&th2, NULL, func, "th2");

  pthread_join(th1, NULL);
  pthread_join(th2, NULL);

  return 0;
}

该代码运行后会死循环打印,我们重点要关注的是亲和性:

$ gcc test.c -pthread
$ ./a.out

a.out运行后,再打开一个新的终端,查询a.out的进程号,然后用taskset命令查看其亲和性,可以看到,亲和性为9(十六进制),转换为二进制为1001,即a.out运行在cpu0和cpu3上。
亲和性展示
或者使用htop来查看亲和性,进入htop后,搜索a.out,查看其亲和性:
htop查看亲和性
可以看到,cpu0和cpu3占用率已经100%了,a.out处于阻塞态,th1和th2分别在cpu3和cpu0上运行。

但是,sched_setaffinity函数的不方便之处在于,无法给线程指定亲和性,要用sched_setaffinity给线程指定亲和性,比较麻烦,需要使用到不可移植的函数gettid()。修改test.c如下:

test.c:使用sched_setaffinity修改线程的CPU亲和性。主要逻辑为,主线程设置亲和性为cpu0,启动两个新线程th1和th2,此时从htop可以看到,主线程阻塞,th1和th2交替运行在cpu0上。然后过一段时间后,线程函数自己修改自己的亲和性(th1将自己的亲和性设置为cpu1,th2将自己的亲和性设置为cpu2),此时从htop可以看到,th1到cpu1上运行,th2到cpu2上运行。

/* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <sched.h>
#include <string.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */

void *func(void *arg)
{
  /* 用来计时 */
  int count = 0;
  pthread_setname_np(pthread_self(), (char *)arg);
  while(1)
  {
    char name[128] = {};
    printf("%s\n", (char *)arg);
    /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
    for(int i=0; i<1000000000; i++);
    count++;
    /* count小于15时,线程都运行在cpu0上;等于15时,线程会设置自身的亲和性,导致后续时间,亲和性就不再是cpu0了 */
    if(count == 15)
    {
      pid_t tid;
      cpu_set_t set;

      CPU_ZERO(&set);
      /* 获取本线程的pid_t类型的ID号,因为sched_setaffinity的参数类型为pid_t */
      tid = syscall(SYS_gettid);
      if(!strncmp((char *)arg, "th1", strlen("th1")+1))
      {
        CPU_SET(1, &set);
        sched_setaffinity(tid, sizeof(cpu_set_t), &set);
      }
      else
      {
        CPU_SET(2, &set);
        sched_setaffinity(tid, sizeof(cpu_set_t), &set);
      }
    }
  }
  return NULL;
}

int main(void)
{
  pthread_t th1, th2;
  cpu_set_t set;

  CPU_ZERO(&set);
  CPU_SET(0, &set);

  /* 设置主线程亲和性为cpu0,这样的话,默认新线程亲和性也是cpu0 */
  int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
  if (ret)
  {
    perror("sched_setaffinity");
    return -1;
  }

  pthread_create(&th1, NULL, func, "th1");
  pthread_create(&th2, NULL, func, "th2");

  pthread_join(th1, NULL);
  pthread_join(th2, NULL);

  return 0;
}

开始时,都在cpu0上运行:
在cpu0运行

过一段时间,在各自的cpu上运行:
在各自cpu运行

2.2.2 pthread_setaffinity_np

如果要设置已存在的线程的亲和性,就可以考虑使用pthread_setaffinity_np,尽管pthread_setaffinity_np是不可移植的,是gnu独有的。因为就算是考虑到移植性,想要使用sched_setaffinity,也避免不了要使用gettid函数(该函数也是gnu独有)。与使用sched_setaffinity设置线程亲和性相比,使用pthread_setaffinity_np的另一个好处就是,可以在线程外设置任一线程的亲和性(只要知道pthread_t即可),而sched_setaffinity的gettid,需要在线程函数内调用。

test.c:使用pthread_setaffinity_np,在main函数中,对两个子线程设置其亲和性。前10s亲和性都追随主函数(cpu0),10s后使用pthread_setaffinity_np设置th1亲和性为cpu1,th2亲和性为cpu2,结果见htop截图。

/* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <sched.h>
#include <string.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */

void *func(void *arg)
{
  pthread_setname_np(pthread_self(), (char *)arg);
  while(1)
  {
    char name[128] = {};
    printf("%s\n", (char *)arg);
    /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
    for(int i=0; i<1000000000; i++);
  }
  return NULL;
}

int main(void)
{
  pthread_t th1, th2;
  cpu_set_t set;

  CPU_ZERO(&set);
  CPU_SET(0, &set);

  /* 设置主线程亲和性为cpu0,这样的话,默认新线程亲和性也是cpu0 */
  int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
  if (ret)
  {
    perror("sched_setaffinity");
    return -1;
  }

  pthread_create(&th1, NULL, func, "th1");
  pthread_create(&th2, NULL, func, "th2");

  /* 前10s,亲和性都是cpu0 */
  sleep(10);
  /* 10s过后,亲和性被修改 */

  /* 移除CPU集合中的cpu0,此时集合中没有任何CPU */
  CPU_CLR(0, &set);
  /* 增加cpu1,此时集合中只有cpu1 */
  CPU_SET(1, &set);
  /* 设置th1的亲和性为cpu1 */
  pthread_setaffinity_np(th1, sizeof(cpu_set_t), &set);

  /* 移除集合中的cpu1,此时集合中没有任何CPU */
  CPU_CLR(1, &set);
  /* 增加cpu2,此时集合中只有cpu2 */
  CPU_SET(2, &set);
  /* 设置th2的亲和性为cpu2 */
  pthread_setaffinity_np(th2, sizeof(cpu_set_t), &set);

  pthread_join(th1, NULL);
  pthread_join(th2, NULL);

  return 0;
}

前10s亲和性:cpu0
前10s亲和性为cpu0

10s后亲和性:th1在cpu1上,th2在cpu2上。
10秒后亲和性发生改变

2.2.3 pthread_attr_setaffinity_np

创建新线程时,通过属性结构体,控制新线程的亲和性。

test.c:

/* 该代码只是为了验证亲和性,故多数函数均没有检查返回值 */
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#include <sched.h>
#include <string.h>
#include <sys/syscall.h>   /* For SYS_xxx definitions */

void *func(void *arg)
{
  pthread_setname_np(pthread_self(), (char *)arg);
  while(1)
  {
    char name[128] = {};
    printf("%s\n", (char *)arg);
    /* 死循环耗时,避免打印过快,不同的机器可能需要调整循环次数 */
    for(int i=0; i<1000000000; i++);
  }
  return NULL;
}

int main(void)
{
  pthread_t th1;
  cpu_set_t set;
  pthread_attr_t attr;

  CPU_ZERO(&set);
  CPU_SET(0, &set);

  /* 设置主线程亲和性为cpu0,这样的话,默认新线程亲和性也应该是cpu0 */
  /* 但是由于新线程创建的时候,使用属性指定了亲和性,就会发现新线程启动之后直接
   * 就在指定的cpu运行了 */
  int ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &set);
  if (ret)
  {
    perror("sched_setaffinity");
    return -1;
  }

  pthread_attr_init(&attr);
  CPU_ZERO(&set);
  CPU_SET(1, &set);
  pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &set);

  pthread_create(&th1, &attr, func, "th1");

  pthread_join(th1, NULL);

  return 0;
}

运行结果:通过属性控制新线程亲和性为cpu1
属性控制线程亲和性

  • 13
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值