《深入浅出DPDK》—第1章1.7节实例

1.7 实例
在对DPDK的原理和代码展开进一步解析之前,先看一些小而简单的例子,建立一个形象上的认知。
1)helloworld,启动基础运行环境,DPDK构建了一个基于操作系统的,但适合包处理的软件运行环境,你可以认为这是个mini-OS。最早期DPDK,可以完全运行在没有操作系统的物理核(bare-metal)上,这部分代码现在不在主流的开源包中。
2)skeleton,最精简的单核报文收发骨架,也许这是当前世界上运行最快的报文进出测试程序。
3)l3fwd,三层转发是DPDK用于发布性能测试指标的主要应用。
1.7.1 HelloWorld
DPDK里的HelloWorld是最基础的入门程序,代码简短,功能也不复杂。它建立了一个多核(线程)运行的基础环境,每个线程会打印“hello from core #”,core #是由操作系统管理的。如无特别说明,本文里的DPDK线程与硬件线程是一一对应的关系。从代码角度,rte是指runtime environment,eal是指environment abstraction layer。DPDK的主要对外函数接口都以rte_作为前缀,抽象化函数接口是典型软件设计思路,可以帮助DPDK运行在多个操作系统上,DPDK官方支持Linux与FreeBSD。和多数并行处理系统类似,DPDK也有主线程、从线程的差异。

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;
}

1.初始化基础运行环境
主线程运行入口是main函数,调用了rte_eal_init入口函数,启动基础运行环境。
int rte_eal_init(int argc, char **argv);
入口参数是启动DPDK的命令行,可以是长长的一串很复杂的设置,需要深入了解的读者可以查看DPDK相关的文档与源代码liblibrte_ealcommoneal_common_options.c。对于HelloWorld这个实例,最需要的参数是“-c ”,线程掩码(core mask)指定了需要参与运行的线程(核)集合。rte_eal_init本身所完成的工作很复杂,它读取入口参数,解析并保存作为DPDK运行的系统信息,依赖这些信息,构建一个针对包处理设计的运行环境。主要动作分解如下

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

对于DPDK库的使用者,这些操作已经被EAL封装起来,接口清晰。如果需要对DPDK进行深度定制,二次开发,需要仔细研究内部操作,这里不做详解。
2.多核运行初始化
DPDK面向多核设计,程序会试图独占运行在逻辑核(lcore)上。main函数里重要的是启动多核运行环境,RTE_LCORE_FOREACH_SLAVE(lcore_id)如名所示,遍历所有EAL指定可以使用的lcore,然后通过rte_eal_remote_launch在每个lcore上,启动被指定的线程。
int rte_eal_remote_launch(int (f)(void ),

void *arg, unsigned slave_id);

第一个参数是从线程,是被征召的线程;
第二个参数是传给从线程的参数;
第三个参数是指定的逻辑核,从线程会执行在这个core上。
具体来说,int rte_eal_remote_launch(lcore_hello, NULL, lcore_id);
参数lcore_id指定了从线程ID,运行入口函数lcore_hello。
运行函数lcore_hello,它读取自己的逻辑核编号(lcore_id),打印出“hello from core #”

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;
}

这是个简单示例,从线程很快就完成了指定工作,在更真实的场景里,这个从线程会是一个循环运行的处理过程。
1.7.2 Skeleton
DPDK为多核设计,但这是单核实例,设计初衷是实现一个最简单的报文收发示例,对收入报文不做任何处理直接发送。整个代码非常精简,可以用于平台的单核报文出入性能测试。
主要处理函数main的处理逻辑如下(伪码),调用rte_eal_init初始化运行环境,检查网络接口数,据此分配内存池rte_pktmbuf_pool_create,入口参数是指定rte_socket_id(),考虑了本地内存使用的范例。调用port_init(portid, mbuf_pool)初始化网口的配置,最后调用lcore_main()进行主处理流程。

int main(int argc, char *argv[])
{
    struct rte_mempool *mbuf_pool;
    unsigned nb_ports;
    uint8_t portid;

    /* Initialize the Environment Abstraction Layer (EAL). */
    int ret = rte_eal_init(argc, argv);

    /* Check that there is an even number of ports t send/receive on. */
    nb_ports = rte_eth_dev_count();
    if (nb_ports < 2 || (nb_ports & 1))
        rte_exit(EXIT_FAILURE, "Error: number of ports must be even\n");

    /* Creates a new mempool in memory to hold the mbufs. */
    mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS * nb_ports,
        MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());

    /* Initialize all ports. */
    for (portid = 0; portid < nb_ports; portid++)
        if (port_init(portid, mbuf_pool) != 0)
            rte_exit(EXIT_FAILURE, "Cannot init port %"PRIu8 "\n",
                    portid);

    /* Call lcore_main on the master core only. */
    lcore_main();
    return 0;
}

网口初始化流程:
port_init(uint8_t port, struct rte_mempool *mbuf_pool)
首先对指定端口设置队列数,基于简单原则,本例只指定单队列。在收发两个方向上,基于端口与队列进行配置设置,缓冲区进行关联设置。如不指定配置信息,则使用默认配置。
网口设置:对指定端口设置接收、发送方向的队列数目,依据配置信息来指定端口功能
int rte_eth_dev_configure(uint8_t port_id, uint16_t nb_rx_q,

        uint16_t nb_tx_q, const struct rte_eth_conf *dev_conf)

队列初始化:对指定端口的某个队列,指定内存、描述符数量、报文缓冲区,并且对队列进行配置

int rte_eth_rx_queue_setup(uint8_t port_id, uint16_t rx_queue_id,
              uint16_t nb_rx_desc, unsigned int socket_id,
              const struct rte_eth_rxconf *rx_conf,
              struct rte_mempool *mp)
int rte_eth_tx_queue_setup(uint8_t port_id, uint16_t tx_queue_id,
              uint16_t nb_tx_desc, unsigned int socket_id,
              const struct rte_eth_txconf *tx_conf)

网口设置:初始化配置结束后,启动端口int rte_eth_dev_start(uint8_t port_id);
完成后,读取MAC地址,打开网卡的混杂模式设置,允许所有报文进入。

static inline int
port_init(uint8_t port, struct rte_mempool *mbuf_pool)
{
    struct rte_eth_conf port_conf = port_conf_default;
    const uint16_t rx_rings = 1, tx_rings = 1;

    /* Configure the Ethernet device. */
    retval = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);

    /* Allocate and set up 1 RX queue per Ethernet port. */
    for (q = 0; q < rx_rings; q++) {
        retval = rte_eth_rx_queue_setup(port, q, RX_RING_SIZE,
                rte_eth_dev_socket_id(port), NULL, mbuf_pool);
    }

    /* Allocate and set up 1 TX queue per Ethernet port. */
    for (q = 0; q < tx_rings; q++) {
        retval = rte_eth_tx_queue_setup(port, q, TX_RING_SIZE,
                rte_eth_dev_socket_id(port), NULL);
    }

    /* Start the Ethernet port. */
    retval = rte_eth_dev_start(port);

    /* Display the port MAC address. */
    struct ether_addr addr;
    rte_eth_macaddr_get(port, &addr);

    /* Enable RX in promiscuous mode for the Ethernet device. */
    rte_eth_promiscuous_enable(port);
    return 0;
}

网口收发报文循环收发在lcore_main中有个简单实现,因为是示例,为保证性能,首先检测CPU与网卡的Socket是否最优适配,建议使用本地CPU就近操作网卡,后续章节有详细说明。数据收发循环非常简单,为高速报文进出定义了burst的收发函数如下,4个参数意义非常直观:端口,队列,报文缓冲区以及收发包数。
基于端口队列的报文收发函数:

static inline uint16_t rte_eth_rx_burst(uint8_t port_id, uint16_t queue_id,
struct rte_mbuf **rx_pkts, const uint16_t nb_pkts)
static inline uint16_t rte_eth_tx_burst(uint8_t port_id, uint16_t queue_id,
struct rte_mbuf **tx_pkts, uint16_t nb_pkts)

这就构成了最基本的DPDK报文收发展示。可以看到,此处不涉及任何具体网卡形态,软件接口对硬件没有依赖。

static __attribute__((noreturn)) void lcore_main(void)
{
    const uint8_t nb_ports = rte_eth_dev_count();
    uint8_t port;
    for (port = 0; port < nb_ports; port++)
        if (rte_eth_dev_socket_id(port) > 0 &&
                rte_eth_dev_socket_id(port) !=
                        (int)rte_socket_id())
            printf("WARNING, port %u is on remote NUMA node to "
                    "polling thread.\n\tPerformance will "
                    "not be optimal.\n", port);

    /* Run until the application is quit or killed. */
    for (;;) {
        /*
         * Receive packets on a port and forward them on the paired
         * port. The mapping is 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2, etc.
         */
        for (port = 0; port < nb_ports; port++) {

            /* Get burst of RX packes, from first port of pair. */
            struct rte_mbuf *bufs[BURST_SIZE];
            const uint16_t nb_rx = rte_eth_rx_burst(port, 0,
                    bufs, BURST_SIZE);

            if (unlikely(nb_rx == 0))
                continue;

            /* Send burst of TX packets, to second port of pair. */
            const uint16_t nb_tx = rte_eth_tx_burst(port ^ 1, 0,
                    bufs, nb_rx);

            /* Free any unsent packets. */
            if (unlikely(nb_tx < nb_rx)) {
                uint16_t buf;
                for (buf = nb_tx; buf < nb_rx; buf++)
                    rte_pktmbuf_free(bufs[buf]);
            }
        }
    }
}
}

1.7.3 L3fwd
这是DPDK中最流行的例子,也是发布DPDK性能测试的例子。如果将PCIE插槽上填满高速网卡,将网口与大流量测试仪表连接,它能展示在双路服务器平台具备200Gbit/s的转发能力。数据包被收入系统后,会查询IP报文头部,依据目标地址进行路由查找,发现目的端口,修改IP头部后,将报文从目的端口送出。路由查找有两种方式,一种方式是基于目标IP地址的完全匹配(exact match),另一种方式是基于路由表的最长掩码匹配(Longest Prefix Match,LPM)。三层转发的实例代码文件有2700多行(含空行与注释行),整体逻辑其实很简单,是前续HelloWorld与Skeleton的结合体。
启动这个例子,指定命令参数格式如下:
./build/l3fwd [EAL options] -- -p PORTMASK [-P]
--config(port,queue,lcore)[,(port,queue,lcore)]
命令参数分为两个部分,以“--”为分界线,分界线右边的参数是三层转发的私有命令选项。左边则是DPDK的EAL Options。

  • [EAL Options]是DPDK运行环境的输入配置选项,输入命令会交给rte_eal_init处理;
  • PORTMASK依据掩码选择端口,DPDK启动时会搜索系统认识的PCIe设备,依据黑白名单原则来决定是否接管,早期版本可能会接管所有端口,断开网络连接。
  • config选项指定(port,queue,lcore),用指定线程处理对应的端口的队列。要实现200Gbit/s的转发,需要大量线程(核)参与,并行转发。

    先来看主线程流程main的处理流程,因为和HelloWorld与Skeleton类似,不详细叙述。
    初始化运行环境: rte_eal_init(argc, argv);
    分析入参: parse_args(argc, argv)
    初始化lcore与port配置
    端口与队列初始化,类似Skeleton处理
    端口启动,使能混杂模式
    启动从线程,令其运行main_loop()
    从线程执行main_loop()的主要步骤如下:
    读取自己的lcore信息完成配置;
    读取关联的接收与发送队列信息;
    进入循环处理:
    {
      向指定队列批量发送报文;
      从指定队列批量接收报文;
      批量转发接收到报文;
    }
    向指定队列批量发送报文,从指定队列批量接收报文,此前已经介绍了DPDK的收发函数。批量转发接收到的报文是处理的主体,提供了基于Hash的完全匹配转发,也可以基于最长匹配原则(LPM)进行转发。转发路由查找方式可以由编译配置选择。除了路由转发算法的差异,下面的例子还包括基于multi buffer原理的代码实现。在#if (ENABLE_MULTI_BUFFER_OPTIMIZE == 1)的路径下,一次处理8个报文。和普通的软件编程不同,初次见到的程序员会觉得奇怪。它的实现有效利用了处理器内部的乱序执行和并行处理能力,能显著提高转发性能。

    for (j = 0; j < n; j += 8) {
        uint32_t pkt_type =
            pkts_burst[j]->packet_type &
            pkts_burst[j+1]->packet_type &
            pkts_burst[j+2]->packet_type &
            pkts_burst[j+3]->packet_type &
            pkts_burst[j+4]->packet_type &
            pkts_burst[j+5]->packet_type &
            pkts_burst[j+6]->packet_type &
            pkts_burst[j+7]->packet_type;
        if (pkt_type & RTE_PTYPE_L3_IPV4) {
            simple_ipv4_fwd_8pkts(&pkts_burst[j], portid, qconf);
        } else if (pkt_type & RTE_PTYPE_L3_IPV6) {
            simple_ipv6_fwd_8pkts(&pkts_burst[j], portid, qconf);
        } else {
            l3fwd_simple_forward(pkts_burst[j],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+1],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+2],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+3],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+4],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+5],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+6],portid, qconf);
            l3fwd_simple_forward(pkts_burst[j+7],portid, qconf);
            }
        }
        for (; j < nb_rx ; j++) {
            l3fwd_simple_forward(pkts_burst[j],portid, qconf);
        }
    

    依据IP头部的五元组信息,利用rte_hash_lookup来查询目标端口。

    mask0 = _mm_set_epi32(ALL_32_BITS, ALL_32_BITS, ALL_32_BITS, BIT_8_TO_15);
    ipv4_hdr = (uint8_t *)ipv4_hdr + offsetof(struct ipv4_hdr, time_to_live);
    __m128i data = _mm_loadu_si128((__m128i*)(ipv4_hdr));
    /* Get 5 tuple: dst port, src port, dst IP address, src IP address and protocol */
    key.xmm = _mm_and_si128(data, mask0);
    /* Find destination port */
    ret = rte_hash_lookup(ipv4_l3fwd_lookup_struct, (const void *)&key);
    return (uint8_t)((ret < 0)? portid : ipv4_l3fwd_out_if[ret]);
    

    这段代码在读取报文头部信息时,将整个头部导入了基于SSE的矢量寄存器(128位宽),并对内部进行了掩码mask0运算,得到key,然后把key作为入口参数送入rte_hash_lookup运算。同样的操作运算还展示在对IPv6的处理上,可以在代码中参考。
    我们并不计划在本节将读者带入代码陷阱中,实际上本书总体上也没有偏重代码讲解,而是在原理上进行解析。如果读者希望了解详细完整的编程指南,可以参考DPDK的网站。

时间: 2024-10-04 01:32:13

《深入浅出DPDK》—第1章1.7节实例的相关文章

《深入浅出DPDK》—第2章2.9节NUMA系统

2.9 NUMA系统 之前的章节已经简要介绍过NUMA系统,它是一种多处理器环境下设计的计算机内存结构.NUMA系统是从SMP(Symmetric Multiple Processing,对称多处理器)系统演化而来. SMP系统最初是在20世纪90年代由Unisys.Convex Computer(后来的HP).Honeywell.IBM等公司开发的一款商用系统,该系统被广泛应用于Unix类的操作系统,后来又扩展到Windows NT中,该系统有如下特点: 1)所有的硬件资源都是共享的.即每个处

《Google软件测试之道》—第2章2.2节测试认证

本节书摘来自异步社区<Google软件测试之道>一书中的第2章2.2节测试认证,作者[美]James Whittaker , Jason Arbon , Jeff Carollo,更多章节 2.2 测试认证 Patrick Copeland在本书的序中强调了让开发人员参与测试的难度.招聘到技术能力强的测试人员只是刚刚开始的第一步,我们依然需要开发人员参与进来一起做测试.其中我们使用的一个 关键方法就是被称为"测试认证"(译注:Test Certified)的计划.现在回过头

《编程珠玑(第2版•修订版)》—第1章1.7节深入阅读

1.7 深入阅读 这个小练习仅仅是令人痴迷的程序说明问题的冰山一角.要深入研究这个重要的课题,参见Michael Jackson②的Software Requirements & Specifications一书(Addison-Wesley出版社1995年出版).该书用一组独立成章却又相辅相成的短文,以令人愉悦的方式阐述了这个艰涩的课题. 在本章所描述的实例研究中,程序员的主要问题与其说是技术问题,还不如说是心理问题:他不能解决问题,是因为他企图解决错误的问题.问题的最终解决,是通过打破他的概

《深入浅出DPDK》—第3章3.1节并行计算

第3章 并 行 计 算 处理器性能提升主要有两个途径,一个是提高IPC(每个时钟周期内可以执行的指令条数),另一个是提高处理器主频率.每一代微架构的调整可以伴随着对IPC的提高,从而提高处理器性能,只是幅度有限.而提高处理器主频率对于性能的提升作用是明显而直接的.但一味地提高频率很快会触及频率墙,因为处理器的功耗正比于主频的三次方. 所以,最终要取得性能提升的进一步突破,还是要回到提高IPC这个因素.经过处理器厂商的不懈努力,我们发现可以通过提高指令执行的并行度来提高IPC.而提高并行度主要有两

《深入浅出DPDK》—第2章2.6节Cache一致性

2.6 Cache一致性 我们知道,Cache是按照Cache Line作为基本单位来组织内容的,其大小是32(较早的ARM.1990年-2000年早期的x86和PowerPC).64(较新的ARM和x86)或128(较新的Power ISA机器)字节.当我们定义了一个数据结构或者分配了一段数据缓冲区之后,在内存中就有一个地址和其相对应,然后程序就可以对它进行读写.对于读,首先是从内存加载到Cache,最后送到处理器内部的寄存器:对于写,则是从寄存器送到Cache,最后通过内部总线写回到内存.这

《深入浅出DPDK》—第1章1.2节初识DPDK

1.2 初识DPDK 本书介绍DPDK,主要以IA(Intel Architecture)多核处理器为目标平台.在IA上,网络数据包处理远早于DPDK而存在.从商业版的Windows到开源的Linux操作系统,所有跨主机通信几乎都会涉及网络协议栈以及底层网卡驱动对于数据包的处理.然而,低速网络与高速网络处理对系统的要求完全不一样. 1.2.1 IA不适合进行数据包处理吗 以Linux为例,传统网络设备驱动包处理的动作可以概括如下: 数据包到达网卡设备. 网卡设备依据配置进行DMA操作. 网卡发送

《深入浅出DPDK》—第1章1.8节小结

1.8 小结什么是DPDK?相信读完本章,读者应该对它有了一个整体的认识.DPDK立足通用多核处理器,经过软件优化的不断摸索,实践出一套行之有效的方法,在IA数据包处理上取得重大性能突破.随着软硬件解耦的趋势,DPDK已经成为NFV事实上的数据面基石.着眼未来,无论是网络节点,还是计算节点,或是存储节点,这些云服务的基础设施都有机会因DPDK而得到加速.在IT和CT不断融合的过程中,在运营商网络和数据中心网络持续SDN化的过程中,在云基础设施对数据网络性能孜孜不倦的追求中,DPDK将扮演越来越重

《深入浅出DPDK》—第3章3.2节指令并发与数据并行

3.2 指令并发与数据并行前面我们花了较大篇幅讲解多核并发对于整体性能提升的帮助,从本节开始,我们将从另外一个维度--指令并发,站在一个更小粒度的视角,去理解指令级并发对于性能提升的帮助.3.2.1 指令并发现代多核处理器几乎都采用了超标量的体系结构来提高指令的并发度,并进一步地允许对无依赖关系的指令乱序执行.这种用空间换时间的方法,极大提高了IPC,使得一个时钟周期完成多条指令成为可能.图3-6中Haswell微架构流水线是Haswell微架构的流水线参考,从中可以看到Scheduler下挂了

《深入浅出DPDK》—第1章1.1节认识DPDK

第一部分 Part 1DPDK基础篇软件正在统治整个世界.--马克·安德森本书的开始部分会重点介绍DPDK诞生的背景.基本概念.核心算法,并结合实例讲解各种基于IA平台的数据面优化技术,包括相关的网卡加速技术.希望可以帮助初次接触DPDK的读者全面了解DPDK,为后面的阅读打下基础. 第1章 认识DPDK什么是DPDK?对于用户来说,它可能是一个性能出色的包数据处理加速软件库:对于开发者来说,它可能是一个实践包处理新想法的创新工场:对于性能调优者来说,它可能又是一个绝佳的成果分享平台.当下火热的