在centos7虚拟机上运行DPVS发生的CPU亲和性设置问题

问题:

在centos7虚拟机上编译完成dpvs后,运行发现出现了如下错误:

[root@localhost bin]# ./dpvs

fail to set thread affinty: Invalid argument

set_all_thread_affinity failed

 

查看dpvs的main.c文件可以看到报错位置如下:

       // 发生错误

    if (set_all_thread_affinity() != 0) {

        fprintf(stderr, "set_all_thread_affinity failed\n");

        exit(EXIT_FAILURE);

    }

而这个函数的目的是使用CPU affinity将当前进程绑定到所有的CPU核心上,对这一块不是很熟悉,以此来学习一下CPU affinity(CPU亲和力)的相关知识。

准备知识:

逻辑CPU个数的计算:逻辑CPU数量 = 物理CPU数量 x CPU cores x 2(如果支持并开启超线程技术(Hyper-Threading))

关于超线程技术,是指利用特殊的硬件指令,把两个逻辑内核(CPU core)模拟成两个物理芯片,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率,我们常听到的双核四线程/四核八线程指的就是支持超线程技术的CPU

物理CPU的个数:机器上安装的实际CPU, 比如说你的主板上安装了一个8核CPU,那么物理CPU个数就是1个,所以物理CPU个数就是主板上安装的CPU个数。

因此比电脑安装了一块4核CPU,并且支持且开启了超线程(HT)技术,那么逻辑CPU数量 = 1 × 4 × 2 = 8

Linux下CPU的相关信息都存放在/proc/cpuinfo中,相关的命令如下:

# 查看物理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的名称型号

cat /proc/cpuinfo|grep "name"|cut -f2 -d:|uniq

在virtualbox上将虚拟机的处理器数量设置为4个

打开虚拟机,输入相关指令,查看CPU的个数

可以看到,对于虚拟机而言,它的物理CPU的个数是1,CPU核心数是4,逻辑CPU个数为4,因此没有开启超线程支持,所以在virtualbox中设置处理器的数量设置的是逻辑CPU的个数

CPU亲和力(亲和性):

CPU affinity 是一种调度属性(scheduler property), 它可以将一个进程"绑定" 到一个或一组CPU上。

在SMP(Symmetric Multi-Processing对称多处理)架构下,Linux调度器(scheduler)会根据CPU affinity的设置让指定的进程运行在"绑定"的CPU上,而不会在别的CPU上运行。

Linux调度器同样支持自然CPU亲和性(natural CPU affinity): 调度器会试图保持进程在相同的CPU上运行, 这意味着进程通常不会在处理器之间频繁迁移,进程迁移的频率小就意味着产生的负载小。

因为程序的作者比调度器更了解程序,所以我们可以手动地为其分配CPU核,而不会过多地占用CPU0,或是让我们关键进程和一堆别的进程挤在一起,所有设置CPU亲和性可以使某些程序提高性能。

CPU affinity的应用:

在服务器压力特别大,心跳经常丢失从而造成服务超时。但是经过分析发现网络没有问题,心跳网络包都发过来了而且也正常进入了dispatch队列,但是由于dispatch在处理别的request的时候耗时过长,而且在处理别的request的时候还会持有一把全局的锁,导致队列里面的其他queue也无法正常被dispatch。所以可以利用设置CPU亲和性来保证核心进程/线程得到足够的时间片,从而不让服务超时。

但是仅仅做到CPU的亲和性,不隔离CPU,那么就只能减少CPU切换,提高cpu cache的命中率,从而减少内存访问损耗,提高程序的速度。但是这样做只能保证自己不被调度到的别的CPU,却不能阻止其他线程来这个绑定好的CPU,因此可以先“隔核”再“绑核”,关于CPU隔离,主要是通过修改grub来实现的。

CPU affinity的表示方法:

CPU affinity使用位掩码(bitmask)表示,每一位都表示一个CPU, 置1表示"绑定"。最低位表示第一个逻辑CPU, 最高位表示最后一个逻辑CPU。CPU affinity典型的表示方法是使用16进制,具体如下。

0x00000001

    绑定processor 0

0x00000003

    绑定processors 0 and 1

相关编程API:

#include <sched.h>
#include <pthread.h>
        /* 清空cpu_set_t 亲和力的设置都是通过这个集合实现的 因此要先清空 */
        void CPU_ZERO(cpu_set_t *set);
        void CPU_ZERO_S(size_t setsize, cpu_set_t *set);
        
        /* 将第几个cpu设置到这个set中 */
        void CPU_SET(int cpu, cpu_set_t *set);
        void CPU_SET_S(int cpu, size_t setsize, cpu_set_t *set);
        
        /* 将第几个cpu从这个set中移除 */
        void CPU_CLR(int cpu, cpu_set_t *set);
        void CPU_CLR_S(int cpu, size_t setsize, cpu_set_t *set);
        
        /* 检测这个cpu是否在这个set中 */
        int CPU_ISSET(int cpu, cpu_set_t *set);
        int CPU_ISSET_S(int cpu, size_t setsize, cpu_set_t *set);
        
        /* 返回set中cpu的个数 */
        void CPU_COUNT(cpu_set_t *set);
        void CPU_COUNT_S(size_t setsize, cpu_set_t *set);

        /* set设置完成后调用这个函数来将进程和cpu绑定 */
        int sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);
        /* 获取进程的cpu亲和性 */
        int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask);

例子:查看和设置进程的CPU亲和性

查看进程的CPU亲和性
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h> /* sysconf */
#include <stdlib.h> /* exit */
#include <stdio.h>

int main(void)
{
    int i, nrcpus;
    cpu_set_t mask;
    unsigned long bitmask = 0;
    CPU_ZERO(&mask); // 首先对set要初始化
    nrcpus = sysconf(_SC_NPROCESSORS_CONF);// 获取逻辑cpu个数
    printf("nrcpus value: %d\n",nrcpus);

     /* 获取当前线程的亲和力set */
    if (sched_getaffinity(0, sizeof(cpu_set_t), &mask) == -1)
    {
        perror("sched_getaffinity");
        exit(EXIT_FAILURE);
    }

    for (i = 0; i < nrcpus; i++)
    {
        if (CPU_ISSET(i, &mask)) // 用CPU_ISSET查看每个cpu是否绑定
        {
            bitmask |= (unsigned long)0x01 << i; //如果绑定相应位置1
            printf("processor #%d is set\n", i);
        }
    }
    printf("bitmask = %#lx\n", bitmask);
    exit(EXIT_SUCCESS);
}

可见当前进程和所有的cpu都绑定了亲和性

设置进程的CPU亲和性
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h> /* sysconf */
#include <stdlib.h> /* exit */
#include <stdio.h>
int main(void)
{
    int i, nrcpus;
    cpu_set_t mask;
    unsigned long bitmask = 0;
    
    CPU_ZERO(&mask);
    CPU_SET(0, &mask); // cpu0加入到set中
    CPU_SET(2, &mask); // cpu2加入到set中

    /* 通过sched_setaffinity函数,根据cpu_set_t设置亲和性 */
    if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) == -1) 
    {   
        perror("sched_setaffinity");
        exit(EXIT_FAILURE);
}

// 设置完成后查看结果
CPU_ZERO(&mask);
     /* 获取当前线程的亲和力set */
    if (sched_getaffinity(0, sizeof(cpu_set_t), &mask) == -1)
    {
        perror("sched_getaffinity");
        exit(EXIT_FAILURE);
    }
    nrcpus = sysconf(_SC_NPROCESSORS_CONF);
    for (i = 0; i < nrcpus; i++)
    {
        if (CPU_ISSET(i, &mask)) // 用CPU_ISSET查看每个cpu是否绑定
        {
            bitmask |= (unsigned long)0x01 << i; //如果绑定相应位置1
            printf("processor #%d is set\n", i);
        }
    }
    printf("bitmask = %#lx\n", bitmask);
    exit(EXIT_SUCCESS);
}

bitmask是0x5的原因是这个值的第一个比特位和第三个比特位置1,所以值为5

另外taskset命名用于获取或者设定CPU亲和性,taskset命令其实就是使用sched_getaffinity()和sched_setaffinity()接口实现的。

问题分析:

了解了CPU affinity的相关知识,我们来看一下set_all_thread_affinity的内容,分析一下问题产生的原因,一下是源码和注释

static int set_all_thread_affinity(void)
{
    int s;
    lcoreid_t cid;
    pthread_t tid;
    cpu_set_t cpuset;
    unsigned long long cpumask=0;

    tid = pthread_self(); // 获取进程id
    CPU_ZERO(&cpuset); // 初始化set
    
    for (cid = 0; cid < RTE_MAX_LCORE; cid++) //宏定义RTE_MAX_LCOR获取逻辑cpu的个数
        CPU_SET(cid, &cpuset); // 将当进程设置和全部cpu的亲和性
    s = pthread_setaffinity_np(tid, sizeof(cpu_set_t), &cpuset); // 进行设置
    if (s != 0) {
        errno = s;
        perror("fail to set thread affinty"); // 报错
        return -1;
    }
    // 设置完成亲和性后 再读取一下
    CPU_ZERO(&cpuset);
    s = pthread_getaffinity_np(tid, sizeof(cpu_set_t), &cpuset);
    if (s != 0) {
        errno = s;
        perror("fail to get thread affinity");
        return -2;
    }
    for (cid = 0; cid < RTE_MAX_LCORE; cid++) {
        if (CPU_ISSET(cid, &cpuset))
            cpumask |= (1LL << cid);
    }
    printf("current thread affinity is set to %llX\n", cpumask);
    return 0;
}

定位到报错位置,发现是在设置亲和性的时候报错的,可能是cpuset值的问题,cpuset的值是循环调用CPU_SET来赋值的,查看一下RTE_MAX_LCORE的值是多少,printf出来以后竟然是128

但是实际上虚拟机的逻辑CPU只有4个,但是在虚拟机上使用sysconf(_SC_NPROCESSORS_CONF)函数可以获取正确的逻辑CPU的个数,尝试将RTE_MAX_LCORE替换为sysconf(_SC_NPROCESSORS_CONF)。并重新编译,注意要调整/etc/dpvs.conf文件里的内容,从dpvs工程里直接复制的配置文件dpvs.conf.single-nic.sample中worker_defs里一共配置了9个cpu,而虚拟机只有4个,因此只需要保留4个即可

worker_defs {
    <init> worker cpu0 {
        type    master
        cpu_id  0
    }
    <init> worker cpu1 {
        type    slave
        cpu_id  1
        port    dpdk0 {
            rx_queue_ids     0
            tx_queue_ids     0
            ! isol_rx_cpu_ids  9
            ! isol_rxq_ring_sz 1048576
        }
    }
    <init> worker cpu2 {
        type    slave
        cpu_id  2
        port    dpdk0 {
            rx_queue_ids     1
            tx_queue_ids     1
            ! isol_rx_cpu_ids  10
            ! isol_rxq_ring_sz 1048576
        }
    }
    <init> worker cpu3 {
        type        slave
        cpu_id      3
        port        dpdk0 {
            rx_queue_ids     2
            tx_queue_ids     2
            ! isol_rx_cpu_ids  11
            ! isol_rxq_ring_sz 1048576
        }
}
}

dpdk 程序将 cpu 称为 lcore,即逻辑核。分为 master, slave 两种类型,一般 master 做管理相关的,slave cpu 是真正处理业务的核,每个 lcore 可以负责多个网卡的多个队列,但是多个lcore不能负责一个网卡的队列,dpdk 中将网卡叫做 port. rx_queue_ids 和 tx_queue_ids 分别是接收和发送队列号。其中 isol_rx_cpu_ids 表示当前 lcore 专职负责接收数据,isol_rxq_ring_sz 专职接收数据的 ring buffer 大小。

再次启动发现不会出现set_all_thread_affinity failed的错误(不知道这算不算是一个dpvs的bug),启动以后发现又发生如下错误,经过尝试发现一般no memory的错误可以通过加大大页的数量来解决,具体的原因后边会进一步探索。

将大页数增加到2048以后,发现不会报错,但是又会发生如下错误

原因是dpdk0这个网卡(实验时在虚拟机上多开了一个host-only的网卡,把它绑定到dpdk上),只支持tx和rx队列各一个,有可能是这个虚拟网卡的限制,只能支持一个,回顾一下/etc/dpvs.conf里边的内容,发现配置了一个master的lcore以及3各slave的lcore,并且每个lcore分别负责不同的rx和tx队列,而这个网卡最多只支持1个rx和tx队列,因此可以把虚拟机的lcore更改成两个,即一个作为master lcore一个作为slave lcore,这样这个slave的lcore只负责一个队列,修改完成后再次运行

这次启动成功了,最后又打出了start dpdk0 failed的log,但是dpvs没有没有报错退出,按照github上说明查看dpvs是否开启,输入./dpid link show,发现是能显示出来正确信息的,这说明dpvs是成功启动了

但是最后出现的start dpdk0 failed应该还是会对dpvs的运行有影响,后边会继续探索。查看github上的issues发现,有很多人在虚拟机上测试dpvs的时候都会遇到各种各样的问题,可能dpvs对虚拟机的支持不太够,以后会试着用物理机来运行dpvs。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值