DPDK 学习笔记(一)

本文为笔者阅读其他网络资料结合自身所感,如有冒犯,请联系笔者。

目录

1.概述

2.dpdk的突破

2.1 UIO (用户空间的 I/O 技术)

2.2 内存池技术

2.3 大页内存管理

2.4 无锁环形队列

2.5 poll-mode网卡驱动

2.6 CPU 亲和性

2.7 多核调度框架

3.应用

4.部分实现

4.1环形缓冲区

4.1.1 Multiple Producers Enqueue

4.2 内存池

4.3 Mbuf Library

4.4 Poll Mode Driver

5. 初始化流程

5.1 初始化函数

5.2 收发包函数

5.3 初始化流程图

5.4 报文发送接收流程图

6.问题


1.概述

数据平面开发套件(DPDK [1]  ,Data Plane Development Kit)是由6WIND,Intel等多家公司开发,主要基于Linux系统运行,用于快速数据包处理的函数库与驱动集合,可以极大提高数据处理性能和吞吐量,提高数据平面应用程序的工作效率。

DPDK使用了轮询(polling)而不是中断来处理数据包。在收到数据包时,经DPDK重载的网卡驱动不会通过中断通知CPU,而是直接将数据包存入内存,交付应用层软件通过DPDK提供的接口来直接处理,这样节省了大量的CPU中断时间和内存拷贝时间。

2.dpdk的突破

相对传统的基于内核的网络数据处理,dpdk 对从内核层到用户层的网络数据流程进行了重大突破,我们先看看传统的数据流程和 dpdk 中的网络流程有什么不同。

 

传统 Linux 内核网络数据流程:

硬件中断--->取包分发至内核线程--->软件中断--->内核线程在协议栈中处理包--->处理完毕通知用户层

用户层收包-->网络层--->逻辑层--->业务层

dpdk 网络数据流程:

硬件中断--->放弃中断流程

用户层通过设备映射取包--->进入用户层协议栈--->逻辑层--->业务层

 

2.1 UIO (用户空间的 I/O 技术)

dpdk 能够绕过内核协议栈,本质上是得益于 UIO 技术,通过 UIO 能够拦截中断,并重设中断回调行为,从而绕过内核协议栈后续的处理流程。

UIO 设备的实现机制其实是对用户空间暴露文件接口,比如当注册一个 UIO 设备 uioX,就会出现文件 /dev/uioX,对该文件的读写就是对设备内存的读写。除此之外,对设备的控制还可以通过 /sys/class/uio 下的各个文件的读写来完成。

 

2.2 内存池技术

dpdk 在用户空间实现了一套精巧的内存池技术,内核空间和用户空间的内存交互不进行拷贝,只做控制权转移。这样,当收发数据包时,就减少了内存拷贝的开销。

 

2.3 大页内存管理

dpdk 实现了一组大页内存分配、使用和释放的 API,上层应用可以很方便使用 API 申请使用大页内存,同时也兼容普通的内存申请。

2.4 无锁环形队列

dpdk 基于 Linux 内核的无锁环形缓冲 kfifo 实现了自己的一套无锁机制。支持单生产者入列/单消费者出列和多生产者入列/多消费者出列操作,在数据传输的时候,降低性能的同时还能保证数据的同步。

2.5 poll-mode网卡驱动

DPDK网卡驱动完全抛弃中断模式,基于轮询方式收包,避免了中断开销。

**NUMA **

dpdk 内存分配上通过 proc 提供的内存信息,使 CPU 核心尽量使用靠近其所在节点的内存,避免了跨 NUMA 节点远程访问内存的性能问题。

2.6 CPU 亲和性

dpdk 利用 CPU 的亲和性将一个线程或多个线程绑定到一个或多个 CPU 上,这样在线程执行过程中,就不会被随意调度,一方面减少了线程间的频繁切换带来的开销,另一方面避免了 CPU 缓存的局部失效性,增加了 CPU 缓存的命中率。

2.7 多核调度框架

dpdk 基于多核架构,一般会有主从核之分,主核负责完成各个模块的初始化,从核负责具体的业务处理。

 

3.应用

 

4.部分实现

4.1环形缓冲区

4.1.1 Multiple Producers Enqueue

This section explains what occurs when two producers concurrently add an object to the ring. In this example, only the producer head and tail (prod_head and prod_tail) are modified.

本节解释了当两个生产者并发地向环添加一个对象时会发生什么。在本例中,只有生产者头和尾(prod_headprod_tail)被修改.

 

The initial state is to have a prod_head and prod_tail pointing at the same location.

初始状态是使prod_headprod_tail指向相同的位置。

 

4.1.1.1. Multiple Producers Enqueue First Step

On both cores, ring->prod_head and ring->cons_tail are copied in local variables. The prod_next local variable points to the next element of the table, or several elements after in the case of bulk enqueue.

在两个核心上,ring->prod_headring->cons_tail都被复制到局部变量中。局部变量prod_next指向表的下一个元素,或者在批量排队的情况下指向后面的几个元素.

 

If there is not enough room in the ring (this is detected by checking cons_tail), it returns an error.

如果环中没有足够的空间(这是通过检查cons_tail检测到的),则返回一个错误。

 

4.1.1.2. Multiple Producers Enqueue Second Step

The second step is to modify ring->prod_head in the ring structure to point to the same location as prod_next. This operation is done using a Compare And Swap (CAS) instruction, which does the following operations atomically:

第二步是修改环结构中的ring->prod_head,使其指向与prod_next相同的位置。这个操作是使用比较和交换(CAS)指令完成的,它原子地执行以下操作:

 

If ring->prod_head is different to local variable prod_head, the CAS operation fails, and the code restarts at first step.

Otherwise, ring->prod_head is set to local prod_next, the CAS operation is successful, and processing continues.

In the figure, the operation succeeded on core 1, and step one restarted on core 2.

如果ring->prod_head与局部变量prod_head不同,则CAS操作失败,并在重新执行第一步代码。

否则,ring->prod_head被设置为局部prod_next,则CAS操作成功,处理继续进行。

在图中,操作在核心1上成功,步骤1在核心2上重新启动。

 

4.1.1.3 Multiple Producers Enqueue Third Step

The CAS operation is retried on core 2 with success.

core 2上重试CAS操作成功.

The core 1 updates one element of the ring(obj4), and the core 2 updates another one (obj5).

核心1更新环的一个元素(obj4),核心2更新另一个元素(obj5)

 

4.1.1.4. Multiple Producers Enqueue Fourth Step

Each core now wants to update ring->prod_tail. A core can only update it if ring->prod_tail is equal to the prod_head local variable. This is only true on core 1. The operation is finished on core 1.

每个核心现在都想更新ring->prod_tail。核心只能在ring->prod_tail等于局部变量prod_head时更新它。这只适用于core 1。操作在核心1上完成。

 

4.1.1.5. Multiple Producers Enqueue Last Step

Once ring->prod_tail is updated by core 1, core 2 is allowed to update it too. The operation is also finished on core 2.

一旦core 1更新了ring->prod_tail, core 2也允许更新它。该操作也在core 2上完成.

 

4.2 内存池

A memory pool is an allocator of a fixed-sized object. In the DPDK, it is identified by name and uses a mempool handler to store free objects. The default mempool handler is ring based. It provides some other optional services such as a per-core object cache and an alignment helper to ensure that objects are padded to spread them equally on all DRAM or DDR3 channels.

内存池是固定大小对象的分配器。在DPDK中,它是通过名称标识的,并使用一个mempool handler来存储空闲对象。默认的mempool handler是基于环的。它提供了一些其他可选的服务,比如每核对象缓存和对齐助手,以确保填充对象,使它们平均分布在所有DRAMDDR3通道上.

4.3 Mbuf Library

The mbuf library provides the ability to allocate and free buffers (mbufs) that may be used by the DPDK application to store message buffers. The message buffers are stored in a mempool, using the Mempool Library.

mbuf库提供了分配和释放缓冲区(mbufs)的能力,DPDK应用程序可以使用这些缓冲区来存储消息缓冲区。消息缓冲区使用mempool库存储在一个mempool中。

 

A rte_mbuf struct generally carries network packet buffers, but it can actually be any data (control data, events, ...). The rte_mbuf header structure is kept as small as possible and currently uses just two cache lines, with the most frequently used fields being on the first of the two cache lines.

rte_mbuf结构体通常携带网络包缓冲区,但它实际上可以是任何数据(控制数据、事件……)rte_mbuf头结构尽可能小,目前只使用两条缓存线,最常用的字段位于两条缓存线的第一条上。

 

4.3.1. Design of Packet Buffers

For the storage of the packet data (including protocol headers), two approaches were considered:

对于包数据(包括协议头)的存储,考虑两种方式:

 

Embed metadata within a single memory buffer the structure followed by a fixed size area for the packet data.

Use separate memory buffers for the metadata structure and for the packet data.

The advantage of the first method is that it only needs one operation to allocate/free the whole memory representation of a packet. On the other hand, the second method is more flexible and allows the complete separation of the allocation of metadata structures from the allocation of packet data buffers.

在单个内存缓冲区内嵌入元数据,该结构后面是用于包数据的固定大小的区域。对元数据结构和数据包数据使用单独的内存缓冲区。

第一种方法的优点是它只需要一个操作来分配/释放数据包的整个内存表示。另一方面,第二种方法更加灵活,允许将元数据结构的分配与分组数据缓冲区的分配完全分离。

 

The first method was chosen for the DPDK. The metadata contains control information such as message type, length, offset to the start of the data and a pointer for additional mbuf structures allowing buffer chaining.

DPDK选择了第一种方法。元数据包含控制信息,如消息类型、长度、到数据开始位置的偏移量和允许缓冲区链接的其他mbuf结构的指针。

 

Message buffers that are used to carry network packets can handle buffer chaining where multiple buffers are required to hold the complete packet. This is the case for jumbo frames that are composed of many mbufs linked together through their next field.

用于携带网络数据包的消息缓冲区可以处理缓冲区链接,其中需要多个缓冲区来保存完整的数据包。对于由许多通过下一个字段链接在一起的mbuf组成的巨型帧来说,情况就是这样。

 

For a newly allocated mbuf, the area at which the data begins in the message buffer is RTE_PKTMBUF_HEADROOM bytes after the beginning of the buffer, which is cache aligned. Message buffers may be used to carry control information, packets, events, and so on between different entities in the system. Message buffers may also use their buffer pointers to point to other message buffer data sections or other structures.

对于新分配的mbuf,数据在消息缓冲区中开始的区域是缓冲区开始后的RTE_PKTMBUF_HEADROOM字节,这是缓存对齐的。消息缓冲区可用于在系统中的不同实体之间携带控制信息、包、事件等。消息缓冲区也可以使用它们的缓冲区指针来指向其他消息缓冲区数据部分或其他结构。

Ps: mbuf struct RTE_PKTMBUF_HEADROOM 中间有一段priv_room,可以用来自定义拓展数据,适配个性化需求。如加入nbuf 结构体。

 

 

Fig. 10.1 and Fig. 10.2 show some of these scenarios.

 

The Buffer Manager implements a fairly standard set of buffer access functions to manipulate network packets.

 

4.4 Poll Mode Driver

The DPDK includes 1 Gigabit, 10 Gigabit and 40 Gigabit and para virtualized virtio Poll Mode Drivers.

DPDK包括1千兆、10千兆和40千兆和para虚拟化virtio轮询模式驱动程序。

 

A Poll Mode Driver (PMD) consists of APIs, provided through the BSD driver running in user space, to configure the devices and their respective queues. In addition, a PMD accesses the RX and TX descriptors directly without any interrupts (with the exception of Link Status Change interrupts) to quickly receive, process and deliver packets in the user’s application. This section describes the requirements of the PMDs, their global design principles and proposes a high-level architecture and a generic external API for the Ethernet PMDs.

轮询模式驱动程序(PMD)由一些api组成,这些api通过运行在用户空间中的BSD驱动程序提供,用于配置设备及其各自的队列。此外,PMD直接访问RXTX描述符,而不需要任何中断(除了链路状态改变中断),以便在用户的应用程序中快速接收、处理和发送数据包。本节描述了pmd的需求、它们的总体设计原则,并为以太网pmd提出了一个高级架构和通用的外部API

Bifurcated Driver

PMDs which use the bifurcated driver co-exists with the device kernel driver. On such model the NIC is controlled by the kernel, while the data path is performed by the PMD directly on top of the device.

使用分叉驱动程序的pmd与设备内核驱动程序共存。在这样的模型中,NIC由内核控制,而数据路径由PMD直接在设备上执行。

 

要求和假设

用于数据包处理应用程序的DPDK环境允许两种模型,即完成运行和管道运行:

  1. 在运行完成 模型中,将通过API轮询特定端口的RX描述符环以获取数据包。然后,数据包在同一内核上进行处理,并通过API放置在端口的TX描述符环上进行传输。
  2. 在管道 模型中,一个内核通过API轮询一个或多个端口的RX描述符环。数据包被接收并通过环传递到另一个核心。另一个内核继续处理该数据包,然后可以通过API将其放置在端口的TX描述符环上进行传输。

在同步运行到完成模型中,分配给DPDK的每个逻辑核心都执行一个数据包处理循环,该循环包括以下步骤:

  1. 通过PMD接收API检索输入数据包
  2. 一次处理每个收到的数据包,直到转发为止
  3. 通过PMD传输API发送待处理的输出数据包

相反,在异步管线模型中,某些逻辑核心可能专用于检索接收到的数据包,而其他逻辑核心可能专用于处理先前接收到的数据包。接收的数据包通过环在逻辑核心之间交换。数据包检索的循环包括以下步骤:

  1. 通过PMD接收API检索输入数据包
  2. 通过数据包队列将接收到的数据包提供给处理内核

数据包处理的循环包括以下步骤:

  1. 从数据包队列中检索收到的数据包
  2. 处理接收到的数据包,如果转发,则重传

为了避免任何不必要的中断处理开销,执行环境不得使用任何异步通知机制。每当需要且适当时,应通过使用环来尽可能地引入异步通信。

在多核环境中,避免锁争用是关键问题。为了解决此问题,PMD旨在与每个核心的私有资源尽可能地配合使用。例如,如果PMD不支持,则PMD会为每个核心,每个端口维护一个单独的传输队列DEV_TX_OFFLOAD_MT_LOCKFREE。同样,端口的每个接收队列都分配给单个逻辑核心(lcore)并由其轮询。

5. 初始化流程

5.1 初始化函数

int rte_log_register(const char * name)

描述:注册动态日志类型,如果已经注册了相同类型的日志,则返回值与前一个相同。

 

int rte_log_set_level      (      uint32_t     logtype,

uint32_t     level

)     

描述:设置给定log类型的log级别

 

int rte_eal_init   (      int argc,

char **        argv

)

描述:初始化环境抽象层(EAL)。Main 函数运行的第一个dpdk初始化函数。这个函数只在主lcore上执行,尽可能快地在应用程序的main()函数中执行。

 

int rte_eth_dev_callback_register    (      uint16_t     port_id,

enum rte_eth_event_type event,

rte_eth_dev_cb_fn       cb_fn,

void *   cb_arg

)     

描述:为端口事件注册一个回调函数

 

int rte_pdump_init   (      void             )     

描述:初始化抓包处理,注册IPC动作,以便与目标(主)进程通信。

 

struct rte_mempool* rte_mempool_create_empty     (      const char *      name,

unsigned   n,

unsigned   elt_size,

unsigned   cache_size,

unsigned   private_data_size,

int socket_id,

unsigned   flags

)     

描述:创建一个空内存池。内存池被分配并初始化,但是它没有被填充:没有为内存池元素分配内存。用户必须调用rte_mempool_populate_*()将内存块添加到池中。一旦填充完毕,用户可能还想用rte_mempool_obj_iter()初始化每个对象。

 

struct rte_mempool* rte_pktmbuf_pool_create   (      const char *      name,

unsigned   n,

unsigned   cache_size,

uint16_t     priv_size,

uint16_t     data_room_size,

int socket_id

)

描述:创建一个mbuf池。此函数创建并初始化一个包mbuf池。它是rte_mempool函数的包装器。

 

 

__rte_experimental struct rte_mempool* rte_pktmbuf_pool_create_extbuf   (      const char *      name,

unsigned int    n,

unsigned int    cache_size,

uint16_t     priv_size,

uint16_t     data_room_size,

int socket_id,

const struct rte_pktmbuf_extmem *        ext_mem,

unsigned int    ext_num

)     

描述:创建带有外部固定数据缓冲区的mbuf池。

此函数创建并初始化一个包mbuf池,该池仅包含带有外部缓冲区的mbuf。它是rte_mempool函数的包装器。

 

void rte_pktmbuf_pool_init  (      struct rte_mempool *     mp,

void *   opaque_arg

)     

描述:包mbuf池构造函数。在pktmbuf池的情况下,此函数初始化mempool私有数据。这个私有数据是驱动程序所需要的。在使用该函数之前,必须在内存池上调用它,或者在创建池时将其作为回调函数提供给rte_mempool_create()。它可以由用户扩展,例如,提供另一个数据包大小。

 

uint32_t rte_mempool_obj_iter  (      struct rte_mempool *     mp,

rte_mempool_obj_cb_t *     obj_cb,

void *   obj_cb_arg

)     

描述:为每个mempool元素调用一个函数

遍历附加到rte_mempool的所有对象并调用其上的回调函数。

 

uint32_t rte_mempool_mem_iter     (      struct rte_mempool *     mp,

rte_mempool_mem_cb_t * mem_cb,

void *   mem_cb_arg

)     

描述:为每个内存块调用一个函数。遍历附加到rte_mempool的所有内存块,并调用其上的回调函数。

 

__rte_experimental int rte_dev_hotplug_handle_enable     (      void             )     

描述:使能设备的热插拔处理。

 

__rte_experimental int rte_dev_event_monitor_start      (      void             )     

描述:启动设备事件监视。

 

 

__rte_experimental int rte_dev_event_callback_register      (      const char *      device_name,

rte_dev_event_cb_fn   cb_fn,

void *   cb_arg

)     

描述:它为特定设备注册回调。可以同时注册多个回调。

 

 

int rte_flow_isolate (      uint16_t     port_id,

int set,

struct rte_flow_error *   error

)     

描述:将进入流量限制为定义的流规则。

 

const struct rte_eth_rxtx_callback* rte_eth_add_rx_callback      (      uint16_t     port_id,

uint16_t     queue_id,

rte_rx_callback_fn        fn,

void *   user_param

)     

描述:添加要在给定端口和队列上的数据包RX上调用的回调。

 

 

const struct rte_eth_rxtx_callback* rte_eth_add_tx_callback      (      uint16_t     port_id,

uint16_t     queue_id,

rte_tx_callback_fn        fn,

void *   user_param

)     

描述: 在给定端口和队列上添加一个回调函数来调用数据包TX。

 

 

int rte_eth_dev_configure   (      uint16_t     port_id,

uint16_t     nb_rx_queue,

uint16_t     nb_tx_queue,

const struct rte_eth_conf *        eth_conf

)     

描述:配置以太网设备。这个函数必须在以太网API中的任何其他函数之前先调用。当设备处于停止状态时,也可以重新调用此函数。

 

int rte_eth_tx_queue_setup       (      uint16_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_rx_queue_setup      (      uint16_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 *     mb_pool

)     

描述:为以太网设备分配和设置一个接收队列。该函数从与socket_id关联的内存区域为nb_rx_desc分配一个连续的内存块,并使用从内存池mb_pool分配的网络缓冲区初始化每个接收描述符。

 

int rte_eth_dev_start      (      uint16_t     port_id  )     

描述:启动一个以太网设备。设备启动步骤是最后一个步骤,包括设置配置的卸载特性和启动设备的发射和接收单元。Device RTE_ETH_DEV_NOLIVE_MAC_ADDR标志在调用PMD端口开始回调函数之前设置MAC地址。一旦成功,就可以调用以太网API导出的所有基本函数(链路状态、接收/传输等)。

 

int rte_eth_promiscuous_enable     (      uint16_t     port_id  )     

描述:为以太网设备启用混杂模式的接收。

 

int rte_eal_remote_launch  (      lcore_function_t * f,

void *   arg,

unsigned   slave_id

)     

描述:在另一个lcore上启动一个函数。仅在主lcore上执行。

 

5.2 收发包函数

static uint16_t rte_eth_rx_burst (      uint16_t     port_id,

uint16_t     queue_id,

struct rte_mbuf **   rx_pkts,

const uint16_t nb_pkts

)     

描述:从以太网设备的接收队列中检索输入包的突发。检索到的数据包存储在rte_mbuf结构中,rx_pkts数组提供了该结构的指针。

rte_eth_rx_burst()函数循环解析接收队列的RX环,解析到nb_pkts包,对于环中每个完成的RX描述符,它执行以下操作:

根据NIC提供的信息,初始化与RX描述符相关的rte_mbuf数据结构到那个RX描述符中。

将rte_mbuf数据结构存储到rx_pkts数组的下一个条目中。

在初始化时,从与接收队列相关联的内存池中分配一个新的rte_mbuf缓冲区来补充RX描述符。

当检索由控制器分散到多个接收描述符中的输入包时,rte_eth_rx_burst()函数将相关的rte_mbuf缓冲区追加到该包的第一个缓冲区。

 

static uint16_t rte_eth_tx_burst (      uint16_t     port_id,

uint16_t     queue_id,

struct rte_mbuf **   tx_pkts,

uint16_t     nb_pkts

)     

描述:在以太网设备的传输队列上发送一串输出数据包。

调用rte_eth_tx_burst()函数来传输由port_id指定的以太网设备的输出队列queue_id上的输出数据包。nb_pkts参数是rte_mbuf结构的tx_pkts数组中提供的要发送的包的数量,每个包都是从由rte_pktmbuf_pool_create()创建的池中分配的。rte_eth_tx_burst()函数循环发送nb_pkts包,直到传输队列的TX环中可用的传输描述符数量为止。对于每个要发送的数据包,rte_eth_tx_burst()函数执行以下操作:

       拾取传输环中的下一个可用描述符。

如果有的话,释放先前与描述符一起发送的网络缓冲区。

使用*rte_mbuf数据结构中提供的信息初始化传输描述符。

rte_eth_tx_burst()函数负责透明地释放之前发送的数据包的内存缓冲区。该特性由设备配置时提供给rte_eth_dev_configure()函数的tx_free_thresh值驱动。当空闲TX描述符的数量低于这个阈值时,rte_eth_tx_burst()函数必须[尝试]释放那些传输已经有效完成的数据包的rte_mbuf缓冲区。

5.3 初始化流程图

 

 

5.4 报文发送接收流程图

 

其中,第1,2步对应函数rte_eth_rx_burst,4,5步对应函数rte_eth_tx_burst

6.问题

1. https://doc.dpdk.org/guides/prog_guide/rte_flow.html

DPDK的流匹配和芯片级数据匹配是否是一个东西,DPDK 依赖芯片的匹配模式?PV VF 概念同上。

DPDK 流匹配是纯软件上的。PV VF 就是交换芯片上对应模块。

 

2.一个网卡的驱动如何被注册,使用?

初始化时, rte_eal_init调用rte_bus_probe来探测所有的总线类型设备,进而调用到pci_probe探测pci 总线下的设备(例e1000),进而调用e1000的探测函数eth_em_pci_probe。其中网卡-》pci设备-》总线 通过链表向上一级注册,探测设备时,由上一级链表依次向下轮询调用下一级链表。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值