dpdk-多队列机制

参考: https://blog.youkuaiyun.com/wyaibyn/article/details/14109325

随着网络IO的带宽的不断提升,单核CPU不能完全处满足网卡的需求,通过多队列网卡的支持,将各个队列通过中断绑定到不同的cpu核上,以满足带宽处理需求。

常见Intel的网卡有82575、82576,Boardcom的57711等,下面以公司的服务器使用较多的Intel 82575网卡为例,分析一下多队列网卡的硬件的实现。

1.多队列网卡硬件实现

下面是一张网卡,有四个硬件队列queue0,queue1,queue2,queue3。

当收到报文时,通过hash包头的SIP、Sport、DIP、Dport四元组,将一条流总是放到相同的队列。

同时触发与该队列绑定的cpu核上中断。

说明: 上面依赖hash算法实现方式,有可能根据源目的ip+port计算出hash值后,以此hash值为下标到数组中找对应的值。例如,我们使用4个cpu处理一个网卡数据包,那数组中可能存着{0,1,2,3,0,1,2,3......0,1,2,3}这样通过hash值下标就会得到数据包需要交给哪个cpu来处理

 

2.什么是RSS-如何根据源目的ip+port找对应的core的?

           RSS(Receive Side Scaling)是一种能够在多处理器系统下使接收报文在多个CPU之间高效分发的软件技术。

  • 网卡对接收到的报文进行解析,获取IP地址、协议和端口五元组信息
  • 网卡通过配置的HASH函数根据五元组信息计算出HASH值,也可以根据二、三或四元组进行计算。
  • 取HASH值的低几位(这个具体网卡可能不同)作为RETA(redirection table)的索引
  • 根据RETA中存储的值分发到对应的CPU

 

3.在DPDK中配置RSS

        DPDK支持设置静态hash值和配置RETA。 不过DPDK中RSS是基于端口的(即针对某个网卡的),并根据端口的接收队列进行报文分发的。 例如我们在一个端口上配置了3个接收队列(0,1,2)并开启了RSS,那么(redirection table)就是这样的:

{0,1,2,0,1,2,0.........}

运行在不同CPU的应用程序就从不同的接收队列接收报文,这样就达到了报文分发的效果。

在DPDK中通过设置rte_eth_conf中的mq_mode字段来开启RSS功能, rx_mode.mq_mode = ETH_MQ_RX_RSS

当RSS功能开启后,报文对应的rte_pktmbuf中就会存有RSS计算的hash值,可以通过pktmbuf.hash.rss来访问。 这个值可以直接用在后续报文处理过程中而不需要重新计算hash值,如快速转发,标识报文流等。

RETA是运行时可配置的,这样应用程序就可以动态改变CPU对应的接收队列,从而动态调节报文分发。 具体通过PMD模块的驱动进行配置,例如ixgbe_dev_rss_reta_updateixgbe_dev_rss_reta_query

### DPDK 中无锁队列的实现 DPDK 的无锁队列设计基于环形缓冲区(ring buffer),并利用硬件支持的原子操作来确保多核环境下的线程安全。以下是其实现的核心逻辑以及一个简单的示例。 #### 核心机制 1. **环形缓冲区结构** 环形缓冲区由一块连续内存组成,用于存储数据项。通过两个指针 `prod_head` 和 `cons_head` 来分别标记生产者和消费者的当前位置。另一个指针 `prod_tail` 或 `cons_tail` 则记录已完成的操作位置[^1]。 2. **原子操作 CAS (Compare And Swap)** 使用 CAS 操作来更新 `prod_head` 或 `cons_head`,从而避免多个生产者或消费者之间的冲突。如果某个核心试图修改这些指针而失败,则会进入循环重试直到成功[^3]。 3. **屏障指令** 在更新尾部指针之前插入内存屏障 (`rte_smp_wmb()` 或 `rte_smp_rmb()`),以确保所有之前的写操作都已提交到共享内存中[^3]。 4. **分阶段更新** 生产者/消费者分为三个主要步骤:抢占头部指针、执行实际的数据复制、最后更新尾部指针。这种分阶段的方式可以减少竞争窗口大小[^4]。 --- #### 示例代码 下面是一个简化版的 DPDK 无锁队列实现: ```c #include <stdio.h> #include <stdint.h> #include <stdbool.h> #include <string.h> // 假设这是 ring 结构的一部分 struct rte_ring { uint32_t size; uint32_t mask; void **entries; }; static inline bool dpdk_enqueue(struct rte_ring *r, void *obj) { uint32_t prod_head, prod_next, cons_tail; do { // 获取当前生产者的 head prod_head = r->prod.head; cons_tail = r->cons.tail; // 计算下一个可用槽位 prod_next = (prod_head + 1) & r->mask; // 如果队列满了则返回 false if (prod_next == cons_tail) return false; // 尝试使用 CAS 更新 prod_head } while (!__sync_bool_compare_and_swap(&r->prod.head, prod_head, prod_next)); // 数据拷贝 r->entries[prod_head] = obj; // 内存屏障 __sync_synchronize(); // 更新 tail r->prod.tail = prod_next; return true; } int main() { struct rte_ring ring = {0}; ring.size = 8; // 队列容量 ring.mask = ring.size - 1; // 掩码 ring.entries = malloc(ring.size * sizeof(void*)); memset(ring.entries, 0, ring.size * sizeof(void*)); char data[] = "Test Data"; if (dpdk_enqueue(&ring, data)) printf("Enqueue successful\n"); else printf("Queue is full\n"); free(ring.entries); return 0; } ``` --- #### 关键点解析 1. **CAS 抢占头指针** ```c while (!__sync_bool_compare_and_swap(&r->prod.head, prod_head, prod_next)); ``` 这里使用了 GCC 提供的内置函数 `__sync_bool_compare_and_swap` 实现 CAS 操作。只有当 `prod_head` 的值未被其他线程更改时才能成功更新。 2. **内存屏障** ```c __sync_synchronize(); ``` 此处模拟了 DPDK 中使用的内存屏障功能,确保在多核环境下数据一致性[^3]。 3. **掩码运算** ```c prod_next = (prod_head + 1) & r->mask; ``` 利用掩码计算下一索引位置,避免越界访问的同时保持性能高效。 --- ### 性能优化建议 - **预分配内存**:提前分配足够的内存给环形缓冲区,避免动态扩展带来的开销。 - **批量处理**:尽量一次性处理多个元素而非逐个操作,降低上下文切换成本。 - **NUMA 节点绑定**:将队列及其关联的工作负载固定在同一 NUMA 节点上,提升缓存命中率[^4]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值