4. DPDK环形缓冲区操作
任务目标
本案例使用DPDK的rte_ring无锁环形缓冲区,实现多生产者(MP)与多消费者(MC)之间的数据传递。核心是验证:多个线程同时向ring写入数据,多个线程同时从ring读取数据,且所有数据均被正确接收、无丢失、无重复。
环境与依赖
- DPDK版本:20.11+(需编译含
librte_ring库) - 编译依赖:
pkg-config --libs libdpdk或手动链接-lrte_ring -lrte_eal - 运行环境:需root权限(HugePages与EAL初始化)
核心实现
代码创建两个生产者线程和两个消费者线程。每个生产者发送PRODUCER_COUNT个递增编号的数据项(共BULK_SIZE个),消费者接收并校验编号连续性,最终统计接收总数。使用rte_ring_mp_enqueue_burst与rte_ring_mc_dequeue_burst保证多线程安全。
#include <rte_ring.h>
#include <rte_eal.h>
#include <rte_lcore.h>
#include <stdio.h>
#include <stdint.h>
#define RING_SIZE 1024
#define PRODUCER_COUNT 1000
#define BULK_SIZE 32
#define NUM_PRODUCERS 2
#define NUM_CONSUMERS 2
static struct rte_ring *ring;
static volatile int producer_done[NUM_PRODUCERS];
static volatile int consumer_done[NUM_CONSUMERS];
struct prod_ctx {
int id;
uint64_t start_val;
};
struct cons_ctx {
int id;
uint64_t received;
int error;
};
static int producer_thread(void *arg) {
struct prod_ctx *ctx = (struct prod_ctx *)arg;
uint64_t val = ctx->start_val;
unsigned sent_total = 0;
while (sent_total < PRODUCER_COUNT) {
unsigned batch = (PRODUCER_COUNT - sent_total) > BULK_SIZE ?
BULK_SIZE : (PRODUCER_COUNT - sent_total);
unsigned i;
void *objs[BULK_SIZE];
for (i = 0; i < batch; i++)
objs[i] = (void *)(uintptr_t)val++;
unsigned sent = rte_ring_mp_enqueue_burst(ring, objs, batch, NULL);
sent_total += sent;
/* 若ring满,重试剩余部分 */
val -= (batch - sent);
}
producer_done[ctx->id] = 1;
return 0;
}
static int consumer_thread(void *arg) {
struct cons_ctx *ctx = (struct cons_ctx *)arg;
void *objs[BULK_SIZE];
uint64_t expected = ctx->id; /* 每个消费者校验自己的起始编号 */
while (1) {
unsigned received = rte_ring_mc_dequeue_burst(ring, objs, BULK_SIZE, NULL);
if (received == 0) {
/* 所有生产者结束且ring为空则退出 */
int all_done = 1;
for (int i = 0; i < NUM_PRODUCERS; i++) {
if (!producer_done[i]) {
all_done = 0;
break;
}
}
if (all_done && rte_ring_count(ring) == 0)
break;
continue;
}
for (unsigned i = 0; i < received; i++) {
uint64_t val = (uint64_t)(uintptr_t)objs[i];
if (val != expected) {
ctx->error = 1;
printf("Consumer %d: expected %lu, got %lu\n",
ctx->id, expected, val);
}
expected++;
ctx->received++;
}
}
consumer_done[ctx->id] = 1;
return 0;
}
int main(int argc, char **argv) {
int ret = rte_eal_init(argc, argv);
if (ret < 0)
return -1;
ring = rte_ring_create("test_ring", RING_SIZE, SOCKET_ID_ANY,
RING_F_SP_ENQ | RING_F_SC_DEQ);
if (!ring) {
printf("Ring creation failed\n");
return -1;
}
/* 注意:为演示MP/MC,需使用非SP/SC标志 */
rte_ring_free(ring);
ring = rte_ring_create("test_ring", RING_SIZE, SOCKET_ID_ANY, 0);
if (!ring) {
printf("Ring creation failed (MP/MC)\n");
return -1;
}
struct prod_ctx pctx[NUM_PRODUCERS];
struct cons_ctx cctx[NUM_CONSUMERS];
unsigned lcore_id;
unsigned used_lcores = 0;
/* 启动生产者 */
for (int i = 0; i < NUM_PRODUCERS; i++) {
pctx[i].id = i;
pctx[i].start_val = i * PRODUCER_COUNT;
producer_done[i] = 0;
lcore_id = rte_get_next_lcore(lcore_id, 1, 0);
if (lcore_id == RTE_MAX_LCORE) {
printf("Not enough lcores\n");
return -1;
}
rte_eal_remote_launch(producer_thread, &pctx[i], lcore_id);
used_lcores++;
}
/* 启动消费者 */
for (int i = 0; i < NUM_CONSUMERS; i++) {
cctx[i].id = i;
cctx[i].received = 0;
cctx[i].error = 0;
consumer_done[i] = 0;
lcore_id = rte_get_next_lcore(lcore_id, 1, 0);
if (lcore_id == RTE_MAX_LCORE) {
printf("Not enough lcores\n");
return -1;
}
rte_eal_remote_launch(consumer_thread, &cctx[i], lcore_id);
used_lcores++;
}
rte_eal_mp_wait_lcore();
uint64_t total_received = 0;
int has_error = 0;
for (int i = 0; i < NUM_CONSUMERS; i++) {
total_received += cctx[i].received;
if (cctx[i].error)
has_error = 1;
printf("Consumer %d received: %lu\n", i, cctx[i].received);
}
uint64_t total_sent = NUM_PRODUCERS * PRODUCER_COUNT;
printf("Total sent: %lu, Total received: %lu\n",
total_sent, total_received);
if (total_sent == total_received && !has_error)
printf("TEST PASSED\n");
else
printf("TEST FAILED\n");
rte_ring_free(ring);
rte_eal_cleanup();
return 0;
}
编译与运行
# 编译(假设DPDK已安装并通过pkg-config暴露)
gcc -o ring_test ring_test.c $(pkg-config --cflags --libs libdpdk) -lpthread
# 运行(需4个可用lcore,建议预留主核)
sudo ./ring_test -l 0-4
验证方法
- 数据完整性:每个消费者校验接收到的编号是否连续递增(起始编号为消费者ID),若出现不连续则打印错误并置
error标志。 - 总数一致性:程序结束时比较
total_sent(2×1000=2000)与total_received,相等且无错误时输出TEST PASSED。 - 并发正确性:由于使用
rte_ring_mp_enqueue_burst和rte_ring_mc_dequeue_burst,多线程同时操作不会产生数据竞争。若改用SP/SC标志(代码中已注释),则多线程场景下会出错,可对比验证MP/MC的必要性。
预期输出
Consumer 0 received: 1000
Consumer 1 received: 1000
Total sent: 2000, Total received: 2000
TEST PASSED
注:每个消费者接收的具体数量可能不同(取决于调度),但总和必须为2000且无编号错误。