ustack项目文档
ustack项目完整分析文档
源码…这就是个学习demo, 没开源哈, 不过后期我一定会把生产级DPDK用户态协议栈实现的! ---2026.2.19
1. 网络协议头分析
1.1 以太网头部 (Ethernet Header)
以太网帧头包含以下字段:
dst_addr: 目标MAC地址 (6字节)src_addr: 源MAC地址 (6字节)ether_type: 类型字段,标识上层协议 (2字节)- 0x0800: IPv4协议
- 0x0806: ARP协议
- 0x86DD: IPv6协议
1.2 IP头部 (IP Header)
IPv4头部包含以下关键字段:
-
version_ihl: 版本(4位)和首部长度(4位),如0x45表示IPv4,首部长度为20字节 -
type_of_service: 服务类型字段 -
total_length: IP包总长度(首部+数据) -
packet_id: 数据包ID,用于分片重组 -
fragment_offset: 分片偏移量 -
time_to_live: TTL生存时间 -
next_proto_id: 上层协议ID(如IPPROTO_TCP=6, IPPROTO_UDP=17) -
src_addr: 源IP地址 -
dst_addr: 目标IP地址 -
hdr_checksum: 头部校验和
1.3 TCP头部 (TCP Header)
TCP头部包含以下字段:
-
src_port: 源端口号 -
dst_port: 目标端口号 -
sent_seq: 序列号 -
recv_ack: 确认号 -
data_off: 数据偏移(首部长度) -
tcp_flags: TCP标志位 -
SYN: 同步序列号,用于建立连接
-
ACK: 确认标志
-
FIN: 断开连接标志
-
RST: 重置连接
-
PSH: 推送数据
-
URG: 紧急指针
-
rx_win: 接收窗口大小 -
cksum: 校验和
1.4 UDP头部 (UDP Header)
UDP头部包含以下字段:
-
src_port: 源端口号 -
dst_port: 目标端口号 -
dgram_len: UDP包长度 -
dgram_cksum: 校验和
2. 代码实现详解
2.1 项目结构
-
ustack.c: 主要实现文件,包含TCP/UDP协议栈的核心功能 -
ustack_perfect.c: 改进版本的实现 -
Makefile: 编译配置文件
2.2 全局变量定义
// 发送相关全局变量
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; // 全局目标端口
// TCP相关状态变量
uint8_t global_flags; // 全局TCP标志位
uint32_t global_seqnum; // 全局序列号
uint32_t global_acknum; // 全局确认号
uint8_t tcp_status = USTACK_TCP_LISTEN; // TCP状态机
2.3 包编码函数
2.3.1 UDP包编码 (ustack_encoding_udp_pkt)
static int ustack_encoding_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len) {
// [1] 以太网头部编码
struct rte_ether_hdr *eth = (struct rte_ether_hdr*)msg;
rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);
rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);
eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
// [2] IP头部编码
struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1);
ip->version_ihl = 0x45; // IPv4 + 首部长度20字节
ip->type_of_service = 0; // 服务类型
ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr));
ip->packet_id = 0; // 数据包ID
ip->fragment_offset = 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); // 计算校验和
// [3] UDP头部编码
struct rte_udp_hdr *udp = (struct rte_udp_hdr*)(ip + 1);
udp->src_port = global_sport; // 源端口
udp->dst_port = global_dport; // 目标端口
uint16_t udp_len = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);
udp->dgram_len = htons(udp_len); // UDP长度
// [4] 数据填充
rte_memcpy((uint8_t*)(udp + 1), data, udp_len);
udp->dgram_cksum = 0;
udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp); // UDP校验和
return 0;
}
2.3.2 TCP包编码 (ustack_encoding_tcp_pkt)
static int ustack_encoding_tcp_pkt(uint8_t* msg, uint16_t total_len) {
// [1] 以太网头部编码 (同UDP)
// [2] IP头部编码 (同UDP)
// [3] TCP头部编码
struct rte_tcp_hdr* tcp = (struct rte_tcp_hdr*)(ip + 1);
tcp->src_port = global_sport; // 源端口
tcp->dst_port = global_dport; // 目标端口
tcp->sent_seq = htonl(12345); // 序列号 (随机值)
tcp->recv_ack = htonl(global_seqnum + 1); // 确认号(对方序列号+1)
tcp->data_off = 0x50; // 数据偏移,5个32位字(20字节)
tcp->tcp_flags = RTE_TCP_SYN_FLAG | RTE_TCP_ACK_FLAG; // SYN+ACK标志
tcp->rx_win = htons(TCP_INIT_WINDOWS); // 接收窗口大小
tcp->cksum = 0;
tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp); // TCP校验和
return 0;
}
2.4 网卡初始化 (ustack_init_port)
static int ustack_init_port(struct rte_mempool *mbuf_pool) {
// 获取可用网卡数量
uint16_t nb_sys_ports = rte_eth_dev_count_avail();
if (nb_sys_ports == 0) {
rte_exit(EXIT_FAILURE, "No Supported eth found\n");
}
// 获取网卡设备信息
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(global_portid, &dev_info);
// 配置网卡
const int num_rx_queues = 1; // 1个接收队列
const int num_tx_queues = 1; // 1个发送队列
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);
// 设置接收队列
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");
}
// 设置发送队列
struct rte_eth_txconf txq_conf = dev_info.default_txconf;
txq_conf.offloads = port_conf_default.rxmode.offloads;
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");
}
// 启动网卡
if (rte_eth_dev_start(global_portid) < 0) {
rte_exit(EXIT_FAILURE, "Could not start\n");
}
return 0;
}
2.5 主要处理逻辑 (main函数)
2.5.1 初始化阶段
int main (int argc, char* argv[]) {
// 1. EAL初始化: DPDK环境抽象层初始化
if (rte_eal_init(argc, argv) < 0) {
rte_exit(EXIT_FAILURE, "Error with EAL init\n");
}
// 2. 获取网卡MAC地址
struct rte_ether_addr mac;
rte_eth_macaddr_get(global_portid, &mac);
// 3. 创建内存池
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
// 4. 初始化网卡端口
ustack_init_port(mbuf_pool);
}
2.5.2 包处理循环
while (1) {
struct rte_mbuf* mbufs[BURST_SIZE] = {0};
// 从接收队列批量获取数据包
uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
for (int i = 0; i < num_recvd; i++) {
// 解析以太网头部
struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*);
if (eth_hdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
continue; // 非IPv4包,跳过
}
// 解析IP头部
struct rte_ipv4_hdr *ip_hdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr*, sizeof(struct rte_ether_hdr));
if (ip_hdr->next_proto_id == IPPROTO_UDP) {
// 处理UDP包
struct rte_udp_hdr *udp_hdr = (struct rte_udp_hdr*)(ip_hdr + 1);
// 交换地址信息,准备回送
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);
// 发送UDP回送包
#if ENABLE_SEND
// 分配新的mbuf
struct rte_mbuf *tx_mbuf = rte_pktmbuf_alloc(mbuf_pool);
// 调用编码函数
ustack_encoding_udp_pkt(msg, data, total_len);
// 发送包
rte_eth_tx_burst(global_portid, 0, &tx_mbuf, 1);
#endif
} else if (ip_hdr->next_proto_id == IPPROTO_TCP) {
// 处理TCP包
struct rte_tcp_hdr *tcp_hdr = (struct rte_tcp_hdr*)(ip_hdr + 1);
// 提取TCP标志位、序列号、确认号
global_flags = tcp_hdr->tcp_flags;
global_seqnum = ntohl(tcp_hdr->sent_seq);
global_acknum = ntohl(tcp_hdr->recv_ack);
// TCP状态机处理
if (global_flags & RTE_TCP_SYN_FLAG) {
if (tcp_status == USTACK_TCP_LISTEN) {
// 发送SYN+ACK响应
ustack_encoding_tcp_pkt(msg, total_len);
tcp_status = USTACK_TCP_SYN_RCVD;
}
} else if (global_flags & RTE_TCP_ACK_FLAG) {
if (tcp_status == USTACK_TCP_SYN_RCVD) {
tcp_status = USTACK_TCP_ESTABLISHED;
printf("\n==== TCP CONNECT SUCCESS ====\n");
}
}
// 处理数据推送
if (global_flags & RTE_TCP_PSH_FLAG) {
if (tcp_status == USTACK_TCP_ESTABLISHED) {
uint8_t tcp_hdr_len = (tcp_hdr->data_off >> 4) * sizeof(uint32_t);
uint8_t *data = ((uint8_t*)tcp_hdr + tcp_hdr_len);
printf("[TCP]: %s\n", data);
}
}
}
// 释放mbuf
rte_pktmbuf_free(mbufs[i]);
}
}
3. DPDK相关函数API和数据结构详解
3.1 DPDK初始化函数
-
rte_eal_init(argc, argv): 初始化DPDK环境抽象层,配置CPU核心、内存等资源 -
rte_exit(EXIT_FAILURE, "Error message"): DPDK错误退出函数
3.2 内存管理相关
-
struct rte_mempool: DPDK内存池结构,用于高效分配和回收固定大小的内存块 -
相当于内核中的
sk_buff -
rte_pktmbuf_pool_create("mbuf_pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()): 创建mbuf内存池 -
struct rte_mbuf: DPDK数据包缓冲区结构,存储网络数据包 -
rte_pktmbuf_alloc(mbuf_pool): 从内存池分配一个mbuf -
rte_pktmbuf_free(mbufs[i]): 释放mbuf回内存池
3.3 网卡管理相关
-
rte_eth_dev_count_avail(): 获取可用网卡数量 -
struct rte_eth_dev_info: 网卡设备信息结构 -
rte_eth_dev_info_get(global_portid, &dev_info): 获取指定网卡设备的详细信息 -
rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default): 配置网卡端口,设置接收和发送队列数量 -
rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool): 设置接收队列 -
rte_eth_tx_queue_setup(global_portid, 0, 512, rte_eth_dev_socket_id(global_portid), &txq_conf): 设置发送队列 -
rte_eth_dev_start(global_portid): 启动网卡端口 -
rte_eth_macaddr_get(global_portid, &mac): 获取网卡MAC地址
3.4 数据包收发相关
-
rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE): 从指定队列批量接收数据包,返回实际接收数量 -
rte_eth_tx_burst(global_portid, 0, &tx_mbuf, 1): 批量发送数据包 -
BURST_SIZE: 突发处理大小,一次最多处理的数据包数量 -
NUM_MBUFS: 内存池中mbuf的数量
3.5 数据包处理相关
-
rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*): 将mbuf转换为特定类型的指针,获取数据包起始地址 -
rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ether_hdr*, sizeof(struct rte_ether_hdr)): 获取偏移后的指针 -
rte_memcpy(): 内存拷贝函数(类似memcpy) -
rte_cpu_to_be_16(),ntohs(),htonl(): 字节序转换函数
3.6 网络协议头结构
-
struct rte_ether_hdr: 以太网头部结构 -
d_addr: 目标MAC地址 -
s_addr: 源MAC地址 -
ether_type: 以太网类型 -
struct rte_ipv4_hdr: IPv4头部结构 -
version_ihl: 版本和首部长度 -
src_addr,dst_addr: 源/目标IP地址 -
next_proto_id: 下一层协议ID -
struct rte_udp_hdr: UDP头部结构 -
src_port,dst_port: 源/目标端口 -
dgram_len: UDP数据包长度 -
struct rte_tcp_hdr: TCP头部结构 -
src_port,dst_port: 源/目标端口 -
sent_seq: 序列号 -
recv_ack: 确认号 -
tcp_flags: TCP标志位 -
data_off: 数据偏移
3.7 校验和计算函数
-
rte_ipv4_cksum(ip): 计算IPv4头部校验和 -
rte_ipv4_udptcp_cksum(ip, tcp/udp): 计算TCP或UDP校验和
3.8 网络配置相关
-
RTE_ETHER_ADDR_LEN: 以太网地址长度(6字节) -
RTE_ETHER_TYPE_IPV4: IPv4以太网类型值 -
IPPROTO_UDP,IPPROTO_TCP: UDP和TCP协议号 -
RTE_TCP_SYN_FLAG,RTE_TCP_ACK_FLAG,RTE_TCP_PSH_FLAG,RTE_TCP_FIN_FLAG: TCP标志位宏定义 -
RTE_ETHER_MAX_LEN: 以太网最大长度 -
RTE_MBUF_DEFAULT_BUF_SIZE: mbuf默认缓冲区大小
3.9 系统信息获取
-
rte_eth_dev_socket_id(global_portid): 获取网卡所在NUMA节点ID -
rte_socket_id(): 获取当前线程所在NUMA节点ID
4. 面试题及详细分析
4.1 面试题1:请解释DPDK的基本原理及其优势
面试级回答:
DPDK(Dataplane Development Kit)是一个用户态网络数据包处理框架,其核心原理是:
-
轮询模式:通过轮询而非中断方式处理网络数据包,避免中断开销
-
零拷贝:数据包直接从网卡DMA到用户态内存,避免内核与用户态之间的数据拷贝
-
内存池:使用预分配的固定大小内存池(mbfs)进行数据包缓冲,提高内存分配效率
-
大页内存:使用大页内存减少页表项,提高TLB命中率
主要优势:
-
高性能:绕过内核,直接在用户态处理数据包
-
低延迟:减少数据包处理路径
-
高吞吐量:批量处理和优化的内存管理
详细分析:
在ustack项目中,我们使用了rte_mempool内存池来管理数据包缓冲区,rte_eth_rx_burst函数进行批量轮询接收数据包,避免了传统中断驱动模式的开销。
4.2 面试题2:请解释TCP三次握手的详细过程及在代码中的实现
面试级回答:
TCP三次握手过程:
-
客户端发送SYN包,状态变为SYN_SENT
-
服务器收到SYN包,发送SYN+ACK包,状态变为SYN_RCVD
-
客户端收到SYN+ACK包,发送ACK包,状态变为ESTABLISHED
-
服务器收到ACK包,状态变为ESTABLISHED
详细分析:
在ustack代码中:
if (global_flags & RTE_TCP_SYN_FLAG) {
if (tcp_status == USTACK_TCP_LISTEN) { // 收到SYN
// 发送SYN+ACK响应包
ustack_encoding_tcp_pkt(msg, total_len);
tcp_status = USTACK_TCP_SYN_RCVD; // 状态变为SYN_RCVD
}
} else if (global_flags & RTE_TCP_ACK_FLAG) {
if (tcp_status == USTACK_TCP_SYN_RCVD) { // 收到ACK
tcp_status = USTACK_TCP_ESTABLISHED; // 状态变为ESTABLISHED
printf("\n==== TCP CONNECT SUCCESS ====\n");
}
}
代码实现了服务器端的三次握手响应逻辑,当收到SYN包时发送SYN+ACK,收到ACK后连接建立。
4.3 面试题3:请解释DPDK的mbuf结构及其在内存管理中的作用
面试级回答:
mbuf是DPDK中数据包的容器结构,其主要特点:
-
预分配:在初始化时批量分配,避免运行时动态分配开销
-
固定大小:每个mbuf大小固定,便于内存管理
-
池化管理:通过内存池统一管理,提高分配回收效率
-
引用计数:支持数据包的共享和复制
详细分析:
在ustack代码中:
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
struct rte_mbuf *tx_mbuf = rte_pktmbuf_alloc(mbuf_pool);
rte_pktmbuf_free(mbufs[i]);
创建内存池后,通过rte_pktmbuf_alloc分配mbuf,通过rte_pktmbuf_free释放mbuf,实现高效的内存复用。
4.4 面试题4:请解释网络协议栈中各层头部的解析方法
面试级回答:
协议栈头部解析遵循层层剥离的方法:
-
从以太网头部开始,根据ether_type判断上层协议
-
对于IP包,解析IP头部,根据next_proto_id判断传输层协议
-
解析TCP或UDP头部,提取端口号等信息
-
最后获取实际数据内容
详细分析:
在ustack代码中,头部解析过程如下:
// 1. 解析以太网头部
struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*);
// 2. 解析IP头部 (偏移sizeof(struct rte_ether_hdr))
struct rte_ipv4_hdr *ip_hdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr*, sizeof(struct rte_ether_hdr));
// 3. 根据协议ID解析TCP或UDP
if (ip_hdr->next_proto_id == IPPROTO_UDP) {
struct rte_udp_hdr *udp_hdr = (struct rte_udp_hdr*)(ip_hdr + 1); // IP+1跳到UDP头部
uint8_t* data = (uint8_t*)(udp_hdr + 1); // UDP+1跳到数据区域
}
4.5 面试题5:请解释TCP状态机的实现原理及在ustack中的应用
面试级回答:
TCP状态机是TCP连接管理的核心机制,通过状态转换处理连接建立、数据传输和连接终止。主要状态包括:CLOSED、LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT等。每个状态对应特定的处理逻辑和状态转换规则。
详细分析:
在ustack代码中,TCP状态机实现如下:
typedef enum __USTACK_TCP_STATUS{
USTACK_TCP_CLOSED = 0,
USTACK_TCP_LISTEN, // 监听状态
USTACK_TCP_SYN_RCVD, // 收到SYN,等待ACK
USTACK_TCP_SYN_SENT, // 发送SYN
USTACK_TCP_ESTABLISHED, // 连接建立
// ... 其他状态
} USTACK_TCP_STATUS;
uint8_t tcp_status = USTACK_TCP_LISTEN;
// 状态转换逻辑
if (global_flags & RTE_TCP_SYN_FLAG) {
if (tcp_status == USTACK_TCP_LISTEN) {
tcp_status = USTACK_TCP_SYN_RCVD; // LISTEN -> SYN_RCVD
}
} else if (global_flags & RTE_TCP_ACK_FLAG) {
if (tcp_status == USTACK_TCP_SYN_RCVD) {
tcp_status = USTACK_TCP_ESTABLISHED; // SYN_RCVD -> ESTABLISHED
}
}
通过状态变量和标志位检查实现状态转换,确保TCP连接按协议规范正确处理。
4.6 面试题6:请解释用户态协议栈相比内核协议栈的优势和挑战
面试级回答:
优势:
-
性能:绕过内核,减少系统调用和上下文切换开销
-
灵活性:完全控制协议处理逻辑,可定制优化
-
可预测性:避免内核调度的不确定性
-
高吞吐量:用户态优化的数据结构和算法
挑战:
-
开发复杂性:需要实现完整的协议栈功能
-
维护成本:独立维护协议栈实现
-
安全性:用户态处理可能引入安全风险
-
兼容性:需处理各种网络异常情况
详细分析:
ustack项目体现了用户态协议栈的特点:
-
使用DPDK框架绕过内核直接处理数据包
-
实现了基础的TCP/UDP协议处理
-
通过状态机管理TCP连接状态
-
面向特定应用场景,可灵活定制
但项目也存在局限性:
-
仅实现了基本功能,缺少完整的错误处理
-
需要手动处理地址映射(无ARP协议)
-
功能相比内核协议栈有限
5. 总结
ustack项目是一个基于DPDK的用户态TCP/UDP协议栈实现,通过直接操作网卡硬件和用户态内存管理,实现高性能的数据包处理。项目涵盖了网络协议栈的核心概念,包括协议头解析、状态机管理、内存池优化等关键技术。虽然功能相对简单,但体现了用户态网络编程的基本原理和方法。