问题:
在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。