DPDK多线程初步解析

上一篇文章中谈到DPDK是一个高性能的用户态驱动,改变了网卡驱动原先的中断为轮询的模式,那么它的性能到底有多强,用数据来说明吧。

1. DPDK性能有多强

DPDK的一个处理器核每秒可以处理约33M个报文,大概30纳秒处理一个报文,在处理器频率2.7GHz的情况下,处理一个数据报文需要80个时钟周期。

在传统的方法上,一个数据报文到达网口后,会经历如下过程:

  1. 写接受描述符到内存,填充数据缓冲区指针,网卡接收到报文后就根据该地址把报文内容填进去。
  2. 从内存中读取接收描述符(到接收到报文时,网卡会更新该结构),从而确认是否收到报文。
  3. 从接收描述符确认收到报文时,从内存中读取控制结构体的指针,再从内存中读取控制结构体,把从接收描述符中读取的信息填充到该控制结构体。
  4. 更新接收队列寄存器,表示软件接收到了新的报文。
  5. 从内存读取报文头部,决定转发端口。
  6. 从控制结构体把报文信息填入到发送队列发送描述符中,更新发送队列寄存器。
  7. 从内存中读取发送描述符,检查是否有包被硬件发送出去。
  8. 如果有的话,则从内存中读取相应控制结构体,释放数据缓冲区。

在这8个步骤中,有6次内存读,而处理器从一级cache读需要3-5时钟周期,二级是十几个时钟周期,三级是几十个时钟周期,而从内存读取数据,由于收到NUMA架构(可以理解为,内存也分给了不同的核,每个核访问自己的内存特别快,访问别的核的内存则需要很长时间)的影响,尤其是不在一个Socket的核之间的内存读取,会花费很长时间,所以平均访问内存需要的时钟周期大约是几百个。处理一个报文80个时钟周期,就要求数据在cache中,而且一旦不命中,性能会严重下降。

而在操作系统中,最容易造成性能下降的是线程的调度,尤其是核间线程的切换,最容易造成cache miss和cache write back。所以在DPDK中利用的是线程的CPU亲和绑定的方式,来指定任务到不同的核上。再进一步,可以限制一些核不参与Linux的系统调度,这样就可以达到任务独占的目的,最大限度地避免了cache不命中带来的性能下降。

查阅DPDK资料,发现DPDK中的多线程是基于linux系统里的pthread实现的,lcore指的是EAL线程,并且在命令行参数中使用“-c”带十六进制参数作为coremask,该掩码的意义是为二进制数上为1的一位即表示将要绑定独占的线程,例如:掩码是16进制的f,二进制对应为1111,即表示cpu0、cpu1、cpu2、cpu3作为逻辑核为程序所用。

2. lcore的初始化如下:
  1. rte_eal_cpu_init()函数中,通过读取/sys/devices/system/cpu/cpuX/下的相关信息,确定当前系统有哪些核,以及分别属于哪些socket(这里的socket是NUMA架构中socket,不是网络中的套接字)。
  2. eal_parse_args()函数,解析-c参数,确认哪些核是可以用的,并且设置第一个核为MASTER。
  3. 为每一个SLAVE核创建线程,并调用eal_thread_set_affinity()绑定CPU,每个线程的执行的其实是一个主体是while死循环的调用不同模块注册到lcore_config[lcore_id].f的回调函数eal_thread_loop()。

*注:在eal_thread_loop()中,将线程绑定核,然后置于了等待的状态。绑定核函数基于linux原型函数f_pthread_setaffinity_np,在pthread_shim.c中有对各种pthread函数封装的实现。

3. lcore的注册:

不同模块需要调用rte_eal_mp_remote_launch(),将自己的回调函数注册到config[].f中。每个核上的线程都会调用该函数来实现自己的处理函数。lcore启动过程和任务分发如下:

多核任务分发.png

另外,由于现网往往有流量潮汐的影响,所以为了寻求灵活的扩展能力,EAL pthread与逻辑核之间允许打破1:1的绑定关系,允许绑定一个特定的lcore ID或者lcore ID组。


4. 程序解析

在example文件夹中,我们来看一个最简单的hello world程序。它建立了一个多核运行的环境,每个线程都会打印“hello from core #”,有点类似pthread的入门程序。

注意:在DPDK代码中,rte(runtime environment)开头的函数是作为给开发者直接调用的接口,也就是说,只是使用DPDK的话,只要知晓这些函数的参数和作用,会调用即可,eal(environment abstraction layer)是DPDK核心库中提供系统抽象的部分,因为虽然现在的源码是基于linux或者FreeBSD系统运行,但它最早期的代码是不依赖于操作系统的,就像自己本身就是个mini-os一样。

helloword的代码如下:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <sys/queue.h>

#include <rte_memory.h>
#include <rte_memzone.h>
#include <rte_launch.h>
#include <rte_eal.h>
#include <rte_per_lcore.h>
#include <rte_lcore.h>
#include <rte_debug.h>

static int
lcore_hello(__attribute__((unused)) void *arg)
{
    unsigned lcore_id;
    lcore_id = rte_lcore_id();
    printf("hello from core %u\n", lcore_id);
    return 0;
}

int
main(int argc, char **argv)
{
    int ret;
    unsigned lcore_id;

    ret = rte_eal_init(argc, argv);
    if (ret < 0)
        rte_panic("Cannot init EAL\n");

    /* call lcore_hello() on every slave lcore */
    RTE_LCORE_FOREACH_SLAVE(lcore_id) {
        rte_eal_remote_launch(lcore_hello, NULL, lcore_id);
    }

    /* call it on master lcore too */
    lcore_hello(NULL);

    rte_eal_mp_wait_lcore();
    return 0;
}

rte_eal_init(argc, argv)中两个命令行入口参数,可以是一系列很长很复杂的设置,从顶往下追溯:
rte_eal_init→eal_log_level_parse→eal_parse_common_option,发现在该函数中,便是对common opinion进行设置的地方。common opinion如下所示,分别用于命令行设置不同的值。

const char
eal_short_options[] =
    "b:" /* pci-blacklist */
    "c:" /* coremask */
    "d:" /* driver */
    "h"  /* help */
    "l:" /* corelist */
    "m:" /* memory size */
    "n:" /* memory channels */
    "r:" /* memory ranks */
    "v"  /* version */
    "w:" /* pci-whitelist */
    ;

其中最重要的就是-c,设置核掩码,这块内容上面已经说过了,运行效果如下:

掩码为1111.png
掩码为1110.png

整体代码的结构很像pthread写的多线程程序,先rte_eal_init()进行一系列很复杂的初始化工作,在官方文档上写的这些初始化工作包括:

  • 配置初始化
  • 内存初始化
  • 内存池初始化
  • 队列初始化
  • 告警初始化
  • 中断初始化
  • PCI初始化
  • 定时器初始化
  • 检测内存本地化(NUMA)
  • 插件初始化
  • 主线程初始化
  • 轮询设备初始化
  • 建立主从线程通道
  • 将从线程设置为等待模式
  • PCI设备的探测和初始化

然后RTE_LCORE_FOREACH_SLAVE遍历所有EAL指定可以使用lcore,通过rte_eal_remote_launch在每个lcore上,启动指定的线程。

需要注意的是lcore_id是一个unsigned变量,其实际作用就相当于循环变量i,因为宏RTE_LCORE_FOREACH_SLAVE里会启动for循环来遍历所有可用的核。

#define RTE_LCORE_FOREACH_SLAVE(i)                  \
    for (i = rte_get_next_lcore(-1, 1, 0);              \
         i<RTE_MAX_LCORE;                       \
         i = rte_get_next_lcore(i, 1, 0))

在函数rte_eal_remote_launch(int (*f)(void *), void *arg, unsigned slave_id))中,第一个参数是从线程要调用的函数,第二个参数是调用的函数的参数,第三个参数是指定的逻辑核。详细的函数执行过程如下:

int
rte_eal_remote_launch(int (*f)(void *), void *arg, unsigned slave_id)
{
    int n;
    char c = 0;
    int m2s = lcore_config[slave_id].pipe_master2slave[1]; //主线程对从线程的管道,管道是一个大小为2的int数组
    int s2m = lcore_config[slave_id].pipe_slave2master[0]; //从线程对主线程的管道

    if (lcore_config[slave_id].state != WAIT)
        return -EBUSY;

    lcore_config[slave_id].f = f;
    lcore_config[slave_id].arg = arg;

    /* send message */
    n = 0;
    while (n == 0 || (n < 0 && errno == EINTR))
        n = write(m2s, &c, 1);     //此处是调用的linux库函数
    if (n < 0)
        rte_panic("cannot write on configuration pipe\n");

    /* wait ack */
    do {
        n = read(s2m, &c, 1); 
    } while (n < 0 && errno == EINTR);

    if (n <= 0)
        rte_panic("cannot read on configuration pipe\n");

    return 0;
}

lcore_config中的pipe_master2slave[2]和pipe_slave2master[2]分别是主线程到从线程核从线程到主线程的管道,与linux中的管道一样,是一个大小为2的数组,数组的第一个元素为读打开,第二个元素为写打开。在这调用了linux库函数read核write,把c作为消息传递。管道的模型如下图所示:


管道模型

这样,每个从线程通过rte_eal_remote_launch函数运行了自定义函数lcore_hello就打印出了“hello from core #”的输出。

注:此篇文章部分引用自《深入浅出DPDK》中的观点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容