一、前言:为什么要用 DPDK?
在传统的网络程序中,我们通常通过 Linux Socket API (recvfrom / sendto / recv / send)来收发数据包。
但这种方式存在几个天然瓶颈:
-
数据包必须经过 Linux 内核协议栈
-
存在 系统调用开销
-
中断、上下文切换频繁
-
在高性能场景(10GbE、25GbE、100GbE)下容易成为瓶颈
为了解决这些问题,Intel 推出了 DPDK(Data Plane Development Kit)。
一句话理解 DPDK:
👉 它绕过 Linux 内核,直接让用户态程序接管网卡,用轮询的方式"抢"数据包。
本文将基于一份完整可运行的 DPDK 示例代码,实现:
-
UDP 数据包的 抓取 + 打印
-
UDP 数据包的 原样回包
-
TCP 三次握手的 最小状态机
-
TCP 数据的 解析与打印
二、整体功能概览
这份程序启动后,会做下面几件事:
-
初始化 DPDK 环境
-
直接接管网卡
-
轮询接收数据包
-
如果是:
-
UDP 包 → 打印 payload,并原样回包
-
TCP 包 → 手写 TCP 状态机,完成三次握手
-
-
整个过程完全绕过 Linux 内核
你可以把它理解为一个:
"用户态实现的、极简 UDP/TCP 协议栈 Demo"
三、核心宏定义与全局变量说明
1、mbuf 与 burst 相关参数
cpp
#define NUM_MBUFS 4096
#define BURST_SIZE 128
-
NUM_MBUFS
表示一次性创建多少个数据包缓冲区(mbuf)
-
BURST_SIZE
每次从网卡批量收多少个包(批量处理能显著提高性能)
DPDK 的核心思想之一:批量 + 轮询
2、功能开关宏
cpp
#define ENABLE_SEND 1
#define ENABLE_TCP 1
-
ENABLE_SEND:是否开启回包功能 -
ENABLE_TCP:是否处理 TCP 协议
3、TCP 状态机枚举
cpp
typedef enum __USTACK_TCP_STATUS{
USTACK_TCP_STATUS_CLOSED = 0,
USTACK_TCP_STATUS_LISTEN,
USTACK_TCP_STATUS_SYN_RCVD,
USTACK_TCP_STATUS_ESTABLISHED,
...
}USTACK_TCP_STATUS;
这是一个简化版 TCP 状态机,用于:
-
模拟服务器端 TCP 行为
-
支持最基本的三次握手
四、DPDK 初始化流程
1、EAL 初始化(DPDK 的入口)
cpp
rte_eal_init(argc, argv);
它会完成:
-
大页内存初始化
-
CPU 核心绑定
-
PCI 网卡扫描
-
驱动加载
如果这一步失败,后面一切都无法进行
2、创建 mbuf 内存池
cpp
rte_pktmbuf_pool_create(
"mbuf pool",
NUM_MBUFS,
0, 0,
RTE_MBUF_DEFAULT_BUF_SIZE,
rte_socket_id()
);
为什么要这么做?
-
DPDK 不允许你在收包时 malloc
-
所有内存必须提前准备好
-
mbuf 是 DPDK 中 数据包的基本载体
性能的根基就在这里
五、网卡端口初始化
cpp
rte_eth_dev_configure(...)
rte_eth_rx_queue_setup(...)
rte_eth_tx_queue_setup(...)
rte_eth_dev_start(...)
这一整套流程的作用是:
| 步骤 | 作用 |
|---|---|
| dev_configure | 配置 RX / TX 队列数量 |
| rx_queue_setup | 配置接收队列 |
| tx_queue_setup | 配置发送队列 |
| dev_start | 启动网卡 |
从这一刻开始:
Linux 内核协议栈基本"失业"了
六、主循环:DPDK 程序的灵魂
cpp
while(1){
rte_eth_rx_burst(...);
}
为什么是 while(1)?
-
DPDK 不使用中断
-
完全靠 用户态轮询
-
用 CPU 换极致低延迟
接收数据包(批量)
cpp
uint16_t num_recvd = rte_eth_rx_burst(
global_portid, 0,
mbufs, BURST_SIZE
);
-
一次最多收
BURST_SIZE个包 -
返回值是真实收到的数量
七、协议解析:从二层到四层
1、以太网头(L2)
cpp
struct rte_ether_hdr *eth_hdr =
rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
判断是否是 IPv4:
cpp
eth_hdr->ether_type == RTE_ETHER_TYPE_IPV4
2、IP 头(L3)
cpp
struct rte_ipv4_hdr *iphdr =
rte_pktmbuf_mtod_offset(...);
根据 next_proto_id 判断:
-
UDP
-
TCP
八、UDP 处理流程
1、读取 UDP payload
cpp
printf("udp : %s\n", (char*)(udphdr + 1));
2、手动封装 UDP 回包
cpp
ustack_encode_udp_pkt(...)
这个函数里手写了:
-
Ethernet Header
-
IPv4 Header
-
UDP Header
-
校验和计算
这是"造包"的完整过程
3、发送数据包
cpp
rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
九、TCP 处理:最简三次握手实现
1、收到 SYN → 回 SYN-ACK
cpp
if(global_flags & RTE_TCP_SYN_FLAG){
if(tcp_status == LISTEN){
send SYN-ACK;
tcp_status = SYN_RCVD;
}
}
2、收到 ACK → 连接建立
cpp
if(global_flags & RTE_TCP_ACK_FLAG){
if(tcp_status == SYN_RCVD){
tcp_status = ESTABLISHED;
}
}
3、收到 PSH → 读取数据
cpp
uint8_t *data = ((uint8_t*)tcphdr + hdrlen);
printf("tcp data: %s\n", data);
这是 TCP payload 的真实位置
九、完整代码
cpp
// 它绕过了Linux操作系统内核(Kernel),直接接管网卡,不断地从网卡"抢"数据包。
// 如果发现抢到的包是 IPv4 协议下的 UDP 数据包,它就去读取包里的内容并在屏幕上打印出来。
#include <stdio.h> // 标准输入输出库,用于 printf 等
#include <rte_eal.h> // DPDK 环境抽象层 (EAL) 头文件,负责底层资源管理
#include <rte_ethdev.h> // DPDK 以太网设备 API,用于控制网卡收发包
#include <arpa/inet.h> // 提供 IP 地址转换函数,如 inet_ntoa
#include <rte_ip.h> // DPDK 定义的 IP 协议头结构
#include <rte_udp.h> // DPDK 定义的 UDP 协议头结构
int global_portid = 0; // 指定要使用的网卡端口 ID,通常从 0 开始
#define NUM_MBUFS 4096 // 内存池中 mbuf (数据包缓冲区) 的总数量
#define BURST_SIZE 128 // 每次从网卡收发数据包的批量大小 (批量处理减少开销)
#define ENABLE_SEND 1 // 宏开关:是否开启数据包发送功能 (回包)
#define ENABLE_TCP 1 // 宏开关:是否开启 TCP 协议处理
#define TCP_INIT_WINDOWS 14600 // 定义 TCP 初始窗口大小
#if ENABLE_SEND
uint8_t global_smac[RTE_ETHER_ADDR_LEN]; // 全局变量:用于存储源 MAC 地址
uint8_t global_dmac[RTE_ETHER_ADDR_LEN]; // 全局变量:用于存储目的 MAC 地址
uint32_t global_sip; // 全局变量:源 IP 地址 (网络字节序)
uint32_t global_dip; // 全局变量:目的 IP 地址 (网络字节序)
uint16_t global_sport; // 全局变量:源端口号
uint16_t global_dport; // 全局变量:目的端口号
#endif
#if ENABLE_TCP
uint8_t global_flags; // 存储接收到的 TCP 标志位 (SYN, ACK, PSH 等)
uint32_t global_seqnum; // 存储接收到的 TCP 序列号
uint32_t global_acknum; // 存储接收到的 TCP 确认号
// 定义 TCP 连接状态枚举,对应 TCP 状态机
typedef enum __USTACK_TCP_STATUS{
USTACK_TCP_STATUS_CLOSED = 0, // 关闭状态
USTACK_TCP_STATUS_LISTEN, // 监听状态
USTACK_TCP_STATUS_SYN_RCVD, // 收到 SYN 包,半连接状态
USTACK_TCP_STATUS_SYN_SENT, // 发送了 SYN 包 (客户端模式用,此处未用)
USTACK_TCP_STATUS_ESTABLISHED, // 连接已建立
USTACK_TCP_STATUS_FIN_WAIT_1, // 断开连接第一阶段
USTACK_TCP_STATUS_FIN_WAIT_2, // 断开连接第二阶段
USTACK_TCP_STATUS_CLOSING, // 双方同时关闭
USTACK_TCP_STATUS_TIMEWAIT, // 等待足够时间以确保远端收到 ACK
USTACK_TCP_STATUS_CLOSE_WAIT, // 等待本地关闭
USTACK_TCP_STATUS_LAST_ACK // 等待最后的 ACK
}USTACK_TCP_STATUS;
uint8_t tcp_status = USTACK_TCP_STATUS_LISTEN; // 初始状态设置为 LISTEN (服务器模式)
#endif
// 定义默认的端口配置结构体
static const struct rte_eth_conf port_conf_default = {
.rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN } // 设置最大接收包长度为以太网标准最大长度 (1518)
};
// 这是 DPDK 的启动引擎。它会负责绑定 CPU 核心、扫描 PCI 设备(网卡)、配置大页内存。
static int ustack_init_port(struct rte_mempool *mbuf_pool){
uint16_t nb_sys_ports = rte_eth_dev_count_avail(); // 获取系统中可用的 DPDK 网卡端口数量
if(nb_sys_ports == 0){ // 如果没有可用端口
rte_exit(EXIT_FAILURE, "No available ports\n"); // 报错并退出程序
}
struct rte_eth_dev_info dev_info; // 用于存储网卡硬件信息的结构体
rte_eth_dev_info_get(global_portid, &dev_info); // 获取指定端口的硬件信息
const int num_rx_queues = 1; // 配置 1 个接收队列
#if ENABLE_SEND
const int num_tx_queues = 1; // 如果开启发送,配置 1 个发送队列
#else
const int num_tx_queues = 0; // 否则配置 0 个发送队列
#endif
// 配置网卡:端口ID、接收队列数、发送队列数、端口配置参数
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
// 设置接收队列:队列ID 0,队列深度 128,Socket ID,默认配置,内存池
if(rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0){
rte_exit(EXIT_FAILURE, "Could not setup RX queue\n"); // 设置失败则退出
}
#if ENABLE_SEND
struct rte_eth_txconf txq_conf = dev_info.default_txconf; // 获取默认发送配置
txq_conf.offloads = port_conf_default.rxmode.offloads; // 同步 Offload 配置
// 设置发送队列:队列ID 0,队列深度 512
if(rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf) < 0){
rte_exit(EXIT_FAILURE, "Could not setup TX queue\n"); // 设置失败则退出
}
#endif
// 真正启动网卡设备,开始工作
if(rte_eth_dev_start(global_portid) < 0) {
rte_exit(EXIT_FAILURE,"Could not start\n");
}
return 0;
}
// 辅助函数:手动封装 UDP 数据包
static int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len){
// ether header
struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg; // 将缓冲区首地址强制转换为以太网头指针
rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN); // 填入目的 MAC 地址
rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN); // 填入源 MAC 地址
eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 设置上层协议类型为 IPv4 (主机字节序转网络字节序)
// ip header
struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(eth +1); // 指针偏移,跳过以太网头,指向 IP 头
ip->version_ihl = 0x45; // 版本号4,头部长度5 (20字节)
ip->type_of_service = 0; // 服务类型 TOS
ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); // IP 包总长度 (总长减去以太网头)
ip->packet_id = 0; // 包 ID
ip->fragment_offset = 0; // 分片偏移,0 表示不分片
ip->time_to_live = 64; // TTL 生存时间
ip->next_proto_id = IPPROTO_UDP; // 上层协议设置为 UDP
ip->src_addr = global_sip; // 填入源 IP
ip->dst_addr = global_dip; // 填入目的 IP
ip->hdr_checksum = 0; // 计算校验和前先置零
ip->hdr_checksum = rte_ipv4_cksum(ip); // 计算 IP 头部校验和
// udp header
struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1); // 指针偏移,指向 UDP 头
udp->src_port = global_sport; // 填入源端口
udp->dst_port = global_dport; // 填入目的端口
// 计算 UDP 数据长度
uint16_t udp_len = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
udp->dgram_len = htons(udp_len); // 填入 UDP 长度
// uint16_t payload_len = udp_len - sizeof(struct rte_udp_hdr);
rte_memcpy((uint8_t *)(udp + 1), data, udp_len); // 将用户数据拷贝到 UDP 头部之后
udp->dgram_cksum = 0; // 校验和置零
udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp); // 计算 UDP 校验和 (包含伪头部)
return 0;
}
// 辅助函数:手动封装 TCP 数据包 (主要用于发送 SYN-ACK)
static int ustack_encode_tcp_pkt(uint8_t *msg, uint16_t total_len){
// ether header
struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg; // 强转为以太网头
rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN); // 填目的 MAC
rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN); // 填源 MAC
eth->ether_type = htons(RTE_ETHER_TYPE_IPV4); // 类型 IPv4
// ip header
struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(eth +1); // 指向 IP 头
ip->version_ihl = 0x45; // 标准 IPv4 头
ip->type_of_service = 0;
ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr)); // IP 层长度
ip->packet_id = 0;
ip->fragment_offset = 0;
ip->time_to_live = 64;
ip->next_proto_id = IPPROTO_TCP; // 上层协议 TCP
ip->src_addr = global_sip; // 源 IP
ip->dst_addr = global_dip; // 目的 IP
ip->hdr_checksum = 0;
ip->hdr_checksum = rte_ipv4_cksum(ip); // IP 校验和
// tcp header
struct rte_tcp_hdr *tcp = (struct rte_tcp_hdr *)(ip + 1); // 指向 TCP 头
tcp->src_port = global_sport; // 源端口
tcp->dst_port = global_dport; // 目的端口
tcp->sent_seq = htonl(12345); // 填入我方的序列号 (此处写死为示例)
tcp->recv_ack = htonl(global_seqnum + 1); // 填入确认号 (对方 Seq + 1)
tcp->data_off = 0x50; // TCP 头长度 (5 * 4 = 20字节)
tcp->tcp_flags = RTE_TCP_SYN_FLAG | RTE_TCP_ACK_FLAG; // 设置标志位:SYN + ACK
tcp->rx_win = TCP_INIT_WINDOWS; // 设置接收窗口大小
tcp->cksum = 0;
tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp); // 计算 TCP 校验和
return 0;
}
int main(int argc, char *argv[]){
if(rte_eal_init(argc,argv) < 0){ // 初始化 DPDK EAL 环境
rte_exit(EXIT_FAILURE, "EAL initialization failed\n"); // 失败则退出
}
// 造内存池。DPDK 为了快,不会在收包时临时去申请内存(malloc 太慢)。它是在程序启动时,一次性申请好几千个固定大小的内存块(Mbuf)。
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS,0,0,RTE_MBUF_DEFAULT_BUF_SIZE,rte_socket_id());
if(mbuf_pool == NULL){ // 内存池创建失败
rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
}
ustack_init_port(mbuf_pool); // 初始化网卡端口
while(1){ // 死循环,负责不断轮询收包
struct rte_mbuf *mbufs[BURST_SIZE] = {0}; // 定义指针数组,存放接收到的包
// 从网卡接收一批数据包,返回值是实际收到的包数量
uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
if(num_recvd > BURST_SIZE){
rte_exit(EXIT_FAILURE, "Received more packets than burst size\n");
}
int i = 0;
for(i = 0; i < num_recvd; i++){ // 遍历处理每一个收到的包
// 将 mbuf 转换为以太网头指针
struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
// 过滤:如果不是 IPv4 协议,释放内存并跳过
if(eth_hdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)){
rte_pktmbuf_free(mbufs[i]);
continue;
}
// 根据以太网头长度偏移,获取 IPv4 头部指针
struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
// ---> 处理 UDP 数据包
if(iphdr->next_proto_id == IPPROTO_UDP){
struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1); // 获取 UDP 头
#if ENABLE_SEND
// 交换源目 MAC 地址,准备回包
rte_memcpy(global_smac, eth_hdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
rte_memcpy(global_dmac, eth_hdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
// 交换源目 IP 地址
rte_memcpy(&global_sip, &iphdr->dst_addr, sizeof(uint32_t));
rte_memcpy(&global_dip, &iphdr->src_addr, sizeof(uint32_t));
// 交换源目 端口
rte_memcpy(&global_sport, &udphdr->dst_port, sizeof(uint16_t));
rte_memcpy(&global_dport, &udphdr->src_port, sizeof(uint16_t));
struct in_addr addr;
addr.s_addr = iphdr->src_addr;
printf("sip %s:%d --> ", inet_ntoa(addr),ntohs(udphdr->src_port)); // 打印源 IP:Port
addr.s_addr = iphdr->dst_addr;
printf("dip %s:%d --> ", inet_ntoa(addr),ntohs(udphdr->dst_port)); // 打印目 IP:Port
uint16_t length = ntohs(udphdr->dgram_len); // 获取 UDP 数据包总长度
// 计算加上以太网头和 IP 头后的总长度
uint16_t total_len = length + sizeof(struct rte_ether_hdr) + sizeof(struct rte_ipv4_hdr);
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool); // 申请一个新的 mbuf 用于发送
if(!mbuf){
rte_exit(EXIT_FAILURE, "Could not allocate mbuf\n");
}
mbuf->pkt_len = total_len; // 设置 mbuf 包长度属性
mbuf->data_len = total_len; // 设置数据长度
uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t *); // 获取新 mbuf 的数据指针
// 调用 UDP 封装函数,填入数据
ustack_encode_udp_pkt(msg, (uint8_t*)(udphdr + 1), total_len);
rte_eth_tx_burst(global_portid, 0, &mbuf, 1); // 发送回包
#endif
printf("udp : %s\n", (char*)(udphdr + 1)); // 打印 UDP 载荷内容
// ---> 处理 TCP 数据包
}else if(iphdr->next_proto_id == IPPROTO_TCP){
struct rte_tcp_hdr *tcphdr = (struct rte_tcp_hdr *)(iphdr + 1); // 获取 TCP 头
// 同样保存 MAC、IP、端口信息,用于状态记录和回包
rte_memcpy(global_smac, eth_hdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
rte_memcpy(global_dmac, eth_hdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);
rte_memcpy(&global_sip, &iphdr->dst_addr, sizeof(uint32_t));
rte_memcpy(&global_dip, &iphdr->src_addr, sizeof(uint32_t));
rte_memcpy(&global_sport, &tcphdr->dst_port, sizeof(uint16_t));
rte_memcpy(&global_dport, &tcphdr->src_port, sizeof(uint16_t));
global_flags = tcphdr->tcp_flags; // 保存当前包的 TCP 标志位
global_seqnum = ntohl(tcphdr->sent_seq); // 保存序列号
global_acknum = ntohl(tcphdr->recv_ack); // 保存确认号
struct in_addr addr;
addr.s_addr = iphdr->src_addr;
printf("tcp pkt sip %s:%d --> ", inet_ntoa(addr),ntohs(tcphdr->src_port)); // 打印源信息
addr.s_addr = iphdr->dst_addr;
printf("dip %s:%d , flags: %x, seqnum: %d, acknum: %d\n", inet_ntoa(addr),ntohs(tcphdr->dst_port),global_flags,global_seqnum,global_acknum); // 打印目的信息和 TCP 状态
// [三次握手阶段 1] 如果收到 SYN 包
if(global_flags & RTE_TCP_SYN_FLAG){
if(tcp_status == USTACK_TCP_STATUS_LISTEN){ // 且当前处于 LISTEN 状态
// 计算回包长度:Eth + IP + TCP
uint16_t total_len = sizeof(struct rte_ether_hdr) + sizeof(struct rte_tcp_hdr) + sizeof(struct rte_ipv4_hdr);
struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool); // 申请内存
if(!mbuf){
rte_exit(EXIT_FAILURE, "Could not allocate mbuf\n");
}
mbuf->pkt_len = total_len;
mbuf->data_len = total_len;
uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t *);
ustack_encode_tcp_pkt(msg,total_len); // 封装 TCP SYN-ACK 包
rte_eth_tx_burst(global_portid, 0, &mbuf, 1); // 发送出去
tcp_status = USTACK_TCP_STATUS_SYN_RCVD; // 状态迁移:LISTEN -> SYN_RCVD
}
}
// [三次握手阶段 3] 如果收到 ACK 包
if(global_flags & RTE_TCP_ACK_FLAG){
if(tcp_status == USTACK_TCP_STATUS_SYN_RCVD){ // 且当前处于 SYN_RCVD 状态
printf("enter established\n");
tcp_status = USTACK_TCP_STATUS_ESTABLISHED; // 状态迁移:SYN_RCVD -> ESTABLISHED (连接建立)
}
}
// [数据传输阶段] 如果收到 PSH 包 (Push Data)
if(global_flags & RTE_TCP_PSH_FLAG){
printf("enter established: %d\n", tcp_status);
if(tcp_status == USTACK_TCP_STATUS_ESTABLISHED){ // 必须是已连接状态
// 计算 TCP 数据偏移量 (Header Length)
uint8_t hdrlen = (tcphdr->data_off >> 4) * sizeof(uint32_t);
// 定位到 TCP 数据 payload 的起始位置
uint8_t *data = ((uint8_t*)tcphdr + hdrlen);
printf("tcp data: %s\n", data); // 打印 TCP 携带的数据
}
}
}
rte_pktmbuf_free(mbufs[i]); // 重要:处理完毕后释放 mbuf 内存回内存池
}
}
return 0;
}