当前位置: 首页 > news >正文

【Linux高级全栈开发】2.4 自研框架:基于 dpdk 的用户态协议栈的实现

【Linux高级全栈开发】2.4 自研框架:基于 dpdk 的用户态协议栈的实现

一、DPDK 环境搭建与数据收取

1. 环境搭建方式
  • 方法一:直接使用预配置虚拟机(含 DPDK 和 SPDK 环境),适合快速上手。

  • 方法二:手动搭建(以 Ubuntu 为例):

    • 多队列网卡支持:修改虚拟机配置文件 .vmx,将网卡驱动从 e1000 改为 vmxnet3,启用多队列。

    • 大页内存(Hugepage)配置:

      sudo vim /etc/default/grub
      # 在 GRUB_CMDLINE_LINUX 中添加
      default_hugepagesz=1G hugepagesz=2M hugepages=1024
      sudo update-grub && reboot
      
    • DPDK 编译与模块加载:

      # 下载 DPDK 源码(如 19.08 版本)
      cd dpdk/usertools
      ./dpdk-setup.sh # 选择编译目标(如 39. x86_64-native-linux-gcc)
      # 加载模块与绑定网卡
      ./dpdk-setup.sh [43] [44] [45] [46/47] [49] # 按提示操作
      
2. 数据收取核心逻辑
  • DPDK 轮询模式:通过 rte_eth_rx_burst 批量接收数据包,避免中断开销。

  • UIO/VFIO 驱动:

    • UIO:用户态直接操作网卡寄存器,性能高但不支持 IOMMU。
    • VFIO:增强版 UIO,支持 IOMMU 和多中断,安全性更高。
  • 关键代码片段:

    // 初始化端口与内存池
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(...);
    ustack_init_port(mbuf_pool);
    // 循环接收数据包
    while (1) {struct rte_mbuf *mbufs[BURST_SIZE];uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);for (int i=0; i<num_recvd; i++) {// 解析以太网头部与 IP 头部struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*);if (ethhdr->ether_type == RTE_ETHER_TYPE_IPV4) {struct rte_ipv4_hdr *iphdr = ...;// 处理 UDP/TCP 协议}}
    }
    

二、用户态协议栈设计与实现

1. 协议栈核心组件
  • 网卡层:通过 DPDK 直接操作网卡,实现零拷贝数据收发。
  • 链路层(Ethernet):解析以太网帧,处理 ARP 协议(IP 转 MAC)。
  • 网络层(IP):处理 IP 分片、路由选择,支持 ICMP 错误报告。
  • 传输层(UDP/TCP):
    • UDP:无连接协议,直接解析端口号传输数据。
    • TCP:实现三次握手、滑动窗口、超时重传等可靠传输机制。
2. UDP 数据收发实现
  • 数据包构建:手动组装以太网头、IP 头、UDP 头,计算校验和。

    // 构建 UDP 响应包
    static int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len) {struct rte_ether_hdr *eth = (struct rte_ether_hdr*)msg;// 设置 MAC 地址与协议类型eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth+1);ip->next_proto_id = IPPROTO_UDP;// 填充 UDP 头部struct rte_udp_hdr *udp = (struct rte_udp_hdr*)(ip+1);udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp); // 校验和计算rte_memcpy(udp+1, data, udplen);
    }
    
3. TCP 数据收发实现
  • 三次握手:通过状态机(USTACK_TCP_STATUS)管理连接状态(LISTEN → SYN_RCVD → ESTABLISHED)。

  • 滑动窗口与拥塞控制:维护发送窗口和接收窗口,动态调整发送速率。

  • 关键代码:处理 SYN 包:

    if (global_flags & RTE_TCP_SYN_FLAG && tcp_status == USTACK_TCP_STATUS_LISTEN) {// 发送 SYN+ACK 响应ustack_encode_tcp_pkt(msg, total_len);rte_eth_tx_burst(...);tcp_status = USTACK_TCP_STATUS_SYN_RCVD;
    }
    

三、POSIX API 与 epoll 实现

1. POSIX API 模拟
  • socket/bind/listen/accept:通过自定义结构体(如 ng_tcp_stream)模拟内核套接字管理。
  • recv/send:操作发送缓冲区(sndbuf)和接收缓冲区(rcvbuf),结合协议栈逻辑实现数据读写。
2. epoll 机制实现
  • 数据结构:
    • 红黑树(rbtree):存储注册的文件描述符(epitem),支持快速查找与删除。
    • 就绪链表(rdlist):存储就绪事件,通过自旋锁保证并发安全。
  • 核心接口:
    • epoll_create:初始化 eventpoll 结构体,分配锁和条件变量。
    • epoll_ctl:通过红黑树添加 / 删除 / 修改监控事件。
    • epoll_wait:阻塞等待就绪事件,通过条件变量实现超时唤醒。
  • 触发模式:
    • LT(水平触发):默认模式,就绪事件未处理时持续通知。
    • ET(边缘触发):仅在状态变化时通知,需配合非阻塞 I/O 使用。

四、性能优化与场景应用

1. 性能优化点
  • 零拷贝(Zero Copy):DPDK 的 rte_mbuf 直接操作物理内存,避免内核态与用户态数据拷贝。
  • 批量操作rte_eth_rx_burst/rte_eth_tx_burst 批量处理数据包,减少函数调用开销。
  • 大页内存:减少页表项数量,提升内存访问效率。
2. 典型应用场景
  • 高性能服务器:如 Web 服务器、消息中间件(Redis/Nginx 优化)。
  • 网络设备:网关、防火墙、负载均衡器(基于 DPDK 的快速转发)。
  • 实时通信:视频流媒体、游戏服务器(低延迟 UDP 传输)。

五、总结

基于 DPDK 的用户态协议栈通过绕过内核网络栈,显著提升了网络处理性能,适用于对吞吐量和延迟敏感的场景。核心实现包括:

  1. DPDK 环境搭建与网卡直接操作:利用轮询模式和用户态驱动(UIO/VFIO)提升效率。
  2. 协议栈分层实现:从链路层到应用层,手动处理协议逻辑,支持 UDP/TCP 等主流协议。
  3. POSIX API 与 epoll 模拟:兼容传统网络编程接口,便于应用迁移。
  4. 性能优化与并发控制:通过大页内存、批量操作、锁优化等手段提升系统稳定性。
主要内容:
  1. 网卡

  2. 网卡驱动

  3. 协议栈

    1. 以太网
    2. ip协议解析
    3. udp解析
    4. tcp解析
  4. posix api

2.4.0 dpdk的环境搭建、用dpdk实现收取数据

dpdk的环境搭建
方法一:直接下载现成完整虚拟机

完整虚拟机环境下载:
链接:https://pan.baidu.com/s/1qY7_bMgIuefF4Vf75o3nog
提取码:g7on

用户名:king
密码:123456

SPDK的虚拟机
链接:https://pan.baidu.com/s/1LKAVpCdGYTXr7nwuy33AgA
提取码:2qdl

方法二:自己搭建虚拟机
多队列网卡的支持
  • 检查是否支持多队列网卡 cat /proc/interrupts | grep ens33(获取整个机器的终端)

    • 结果 19: 42 0 212 0 IO-APIC 19-fasteoi ens33,不支持多队列网卡

    • 虚拟机关机,修改文件 .vmx文件,找到 ethernet0.virtualDev = "e1000",把 e1000 改成 vmxnet3,并添加 ethernet0.wakeOnPcktRcv = "TRUE"

Hugepage(大页/巨页)
  • 进入 sudo vim /etc/default/grubGRUB_CMDLINE_LINUX= 后 增加三个配置 default_hugepagesz=1G hugepagesz=2M hugepages=1024

  • 然后虚拟机重启 sudo update-grub

用dpdk实现收取数据
环境搭建
  • DPDK源码下载:

    • core.dpdk.org/download/选择 19.08版本下载
  • 进入dpdk/usertools/dpdk-setup.sh编译DPDK环境变量(只需要编译一次) :

    • 如果不需要修改源码,选择 [36] x86_64-native-linuxapp-gcc
    • 如果需要修改源码,选择 [39] x86_64-native-linux-gcc
    • 如果出现报错:ERROR: Target does not have the DPDk uIo Kernel Module。To fix, please try to rebuild target.的解决方法:
      • 设置环境变量1:export RTE_SDK=/home/king/share/dpdk/dpdk-stable-19.08.2/
      • 设置环境变量2:export RTE_TARGET=x86_64-native-linux-gcc
  • 设置Linux环境变量:

    [43] Insert IGB UIO module
    [44] Insert VFIO module
    [45] Insert KNI module
    [46] Setup hugepage mappings for non-NUMA systems
    [47] Setup hugepage mappings for NUMA systems
    [48] Display current Ethernet/Baseband/Crypto device settings
    [49] Bind Ethernet/Baseband/Crypto device to IGB UIO module
    
    • [43] Insert IGB UIO module
      • 功能:IGB UIO(Intel Gigabit Ethernet Userspace I/O)模块是一种用户空间输入 / 输出驱动模块。它允许绕过内核网络栈,直接在用户空间对网卡进行操作。在 DPDK(Data Plane Development Kit )中使用该模块,能使应用程序更高效地处理网络数据包,减少内核态和用户态之间的切换开销,提升数据包处理性能和吞吐量 。比如在高并发的网络服务器场景下,可让 DPDK 应用直接快速地与网卡交互。
    • [44] Insert VFIO module
      • 功能:VFIO(Virtual Function I/O)是一种用于设备虚拟化的框架。插入 VFIO 模块后,可支持将物理设备(如网卡)以更灵活的方式分配给虚拟机或用户空间应用程序。在 DPDK 环境中,它能实现对设备的高效隔离和共享,方便在多租户或虚拟化场景下,让不同的应用或虚拟机安全、独立地使用设备资源,增强系统的灵活性和资源利用率 。
    • [45] Insert KNI module
      • 功能:KNI(Kernel NIC Interface)模块提供了一种在 DPDK 应用和内核网络栈之间建立接口的方式。它允许 DPDK 处理后的数据包能够与内核网络栈进行交互,比如将经过 DPDK 处理的数据包发送到内核网络栈进行进一步的处理(如防火墙规则检查、路由等),或者从内核网络栈接收数据包进行 DPDK 层面的处理。这在一些需要结合用户态高性能处理和内核态网络功能的场景中很有用 。
    • [46] Setup hugepage mappings for non-NUMA systems 这里大页输入512
      • 功能:大页(hugepage)是一种内存分页机制,相比普通的 4KB 页,大页通常为 2MB 或 1GB。对于非 NUMA(Non-Uniform Memory Access,非统一内存访问架构 )系统,设置大页映射可减少内存页表项数量,降低 CPU 在内存地址转换时的开销,提高内存访问效率。在 DPDK 这种对内存读写频繁的场景下,使用大页能显著提升应用性能,减少内存管理的开销 。
    • [47] Setup hugepage mappings for NUMA systems这里大页输入512
      • 功能:NUMA 系统中,不同的 CPU 节点访问内存的速度不同。为 NUMA 系统设置大页映射,除了能享受类似非 NUMA 系统中减少页表项、提升内存访问效率的好处外,还能更好地结合 NUMA 架构特性,让每个 CPU 节点更高效地访问本地内存,进一步优化内存访问性能,使 DPDK 应用在 NUMA 架构的服务器上能充分发挥硬件优势,提升整体性能 。
    • [48] Display current Ethernet/Baseband/Crypto device settings这一步不用执行
      • 功能:该选项用于显示当前以太网设备、基带设备以及加密设备的相关设置信息。比如网卡的工作模式(全双工、半双工等)、速率、MAC 地址,基带设备的参数配置,加密设备的算法、密钥等设置情况。通过查看这些信息,用户可以了解设备当前的运行状态,判断是否符合 DPDK 应用的需求,以便进行相应的调整和优化 。
    • [49] Bind Ethernet/Baseband/Crypto device to IGB UIO module这里选择 eth0对应的pci码 0000:03:00.0,注意需要先 sudo ifconfig eth0 down让网卡下线再插入
      • 功能:将以太网设备、基带设备或加密设备绑定到 IGB UIO 模块,是为了让这些设备能够利用 IGB UIO 模块提供的用户空间直接访问功能。绑定后,DPDK 应用就可以绕过内核网络栈,直接对设备进行操作和数据处理,实现高效的数据包收发和设备控制,从而提升网络数据处理的性能和效率 。
网络结构层级解释

在这里插入图片描述

  1. 网卡(Network Interface Card)
    网卡是计算机与网络进行物理连接的硬件设备,它负责接收和发送物理层的电信号或光信号等,是网络数据进入计算机系统的入口。比如常见的以太网网卡,通过网线连接到网络交换机等设备,将网络中的数据以物理信号形式传输进来。

    • 注:当网络数据包到达网卡时,网卡将数据包存储在内存中,并把数据包的相关信息(如数据指针、长度等)填充到 sk buff 结构体中。随后,sk buff 结构体被传递给驱动程序。驱动程序借助 sk buff 来管理数据包,将其进一步向上层协议栈传递 。这就好比是一个 “包裹”,网卡把收到的网络数据打包放进 sk buff 这个 “包裹” 里,再由驱动程序接力传递。
  2. driver(驱动程序)
    网卡驱动程序是操作系统与网卡硬件之间的桥梁。它的作用是将操作系统的指令转换为网卡能够理解的操作,同时也将网卡接收到的数据或状态信息反馈给操作系统。不同的网卡硬件需要对应的驱动程序才能正常工作,驱动程序负责初始化网卡、设置工作参数以及处理网卡与操作系统之间的数据交互。

  3. tcp/ip(传输控制协议 / 网际协议)
    TCP/IP 是一组网络协议族,是互联网通信的基础。在这个层级,它负责对从网卡接收到的数据进行解析、封装、路由等操作。例如,TCP 协议负责提供可靠的、面向连接的数据传输服务,会对数据进行分段、排序、确认等处理;IP 协议则负责为数据报分配 IP 地址,进行路由选择,让数据能够在不同的网络之间传输。

    • 注:sk buff 结构体贯穿于 TCP/IP 协议栈的各个层次。在网络层,它用于处理 IP 数据包的相关信息,如 IP 地址、协议类型等,帮助 IP 协议进行路由选择等操作;在传输层,用于处理 TCP 或 UDP 数据包的相关信息,比如 TCP 的序列号、端口号等,以实现可靠或不可靠的数据传输。可以理解为在 TCP/IP 这个 “大工厂” 里,sk buff 带着数据包的信息,在不同的 “车间”(网络层、传输层等)流转加工。
  4. posix api(recv ()/send () 等)
    POSIX(Portable Operating System Interface)是一系列标准,定义了操作系统应提供的接口规范。recv()send()是 POSIX 标准中用于网络编程的 API 函数。recv()用于从网络连接中接收数据,send()用于向网络连接发送数据。应用程序通过调用这些 API 来与底层的 TCP/IP 协议栈进行交互,实现数据的收发操作。

    • 注:应用程序通过 POSIX API(如 recv ()、send () )与底层协议栈交互时,sk buff 结构体承载着要接收或发送的数据。
  5. App(如 Redis、Nginx)
    这是基于网络功能开发的具体应用程序。Redis 是一个内存型的键值数据库,它利用网络功能实现客户端与服务端的数据交互,比如接收客户端的命令请求并返回结果;Nginx 是一个高性能的 Web 服务器和反向代理服务器,通过网络接收用户的 HTTP 请求,进行处理后返回相应的网页内容等。

  6. sk buff在各层级间传递数据包信息
    
  7. UIO(Userspace I/O)

    • 概念:是一种用户态驱动框架。Linux 内核提供uio.ko内核模块作为框架支持。原理是为注册的设备生成/dev/uioX字符设备,用户态程序借助它实现设备内存空间映射、中断开关与获取等操作。
    • 作用:在 DPDK(Data Plane Development Kit )中,可让网卡驱动(如 igb_uio ,针对 Intel 网卡)运行在用户态,采用轮询和零拷贝方式从网卡收报文,提高收发性能。它截获中断并重置回调行为,绕过内核协议栈后续处理,还将网卡硬件寄存器映射到用户态。
    • 局限:不支持 IOMMU,若设备需 DMA,用户态要获取并注册物理内存地址空间;每个设备仅支持一个中断,对需多中断响应的应用不太友好。
  8. VFIO(Virtual Function I/O )

    • 概念:是 UIO 的增强版,是全能用户态 IO 框架。借助 IOMMU(Input/Output Memory Management Unit ,输入 / 输出内存管理单元 )实现设备的 DMA 访问和中断重定向。包含接口层框架模块vfio和设备驱动模块(如vfio-pci ,针对 PCI 设备) 。
    • 作用:通过 IOMMU 避免用户态注册物理 DMA 地址的高危操作,将用户态进程虚拟内存地址注册给网卡设备并维护页表;为设备每个 irq 中断提供用户态接收方式(注册 eventfd ),解决了 UIO 的安全和易用性问题,以及中断支持不足问题。在 DPDK 中,是主要的用户态驱动框架。
  9. KNI(Kernel NIC Interface)

    • 概念:是 DPDK 提供的一种模块,用于在 DPDK 应用和内核网络栈间建立接口。
    • 作用:允许 DPDK 处理后的数据包与内核网络栈交互。比如将 DPDK 处理后的包送内核网络栈做防火墙规则检查、路由等;或从内核网络栈接收数据包进行 DPDK 层面处理,结合用户态高性能处理和内核态网络功能 。
编译ustack.c代码
  • 完整代码见下文,使用make编译,如果有 Makefile:73: *** "Please define RTE_SDK environment variable". Stop.报错和 Cause: No Supported eth found报错:

    • 设置编译时环境变量

      export RTE SDK=/home/king/share/dpdk/dpdk-stable-19.08.2/
      export RTE TARGET=x86_64-native-linux-gcc
      
    • 设置运行时环境

      sudo ./share/dpdk/dpdk-stable-19.08.2/usertools/dpdk-setup.sh
      

      运行 39 43 44 45 46(512) 47(512) 49(down eth1)

    • 设置 arp

      • 在Linux虚拟机上输入 ifconfig -a,查看eth 0 的mac地址 HWaddr 00:0c:29:c6:0c:b1,linux上对应为冒号,window上对应为横杠 00-0c-29-c6-0c-b1
      • 在window主机上输入arp -a 插看对应mac地址映射 接口: 10.134.96.172 --- 0x130x13就是十进制的 19
      • 设置静态 arpnetsh -c i i add neighbors 19 10.134.96.77 00-0c-29-c6-0c-b1, 记得用管理员运行
  • 使用 sudo ./build/ustack运行代码

2.4.1 用户态协议栈设计实现

DPDK(Data Plane Development Kit)是一个用于,快速数据包处理的开源库,基于 DPDK 的用户态协议栈实现,是在用户空间而非内核空间来处理网络协议,这样可以避开内核态和用户态切换开销,提升网络处理性能。

  • DPDK的作用

    • **DPDK是用于提升吞吐量的,不是用于提升qps和延迟的。**用于高性能网络开发。
    • 适用场景有:
      • 数据备份
      • 网关防火墙 —— CDN,流媒体服务器等
  • 用户态协议栈的存在场景与实现原理:传统网络协议栈在内核中实现,数据在内核态和用户态间拷贝开销大。用户态协议栈将协议处理放在用户空间,可减少上下文切换和数据拷贝开销。适用于对网络性能要求极高的场景,如网络转发设备、高性能网络服务器等。其原理是利用 DPDK 提供的大页内存管理、轮询模式驱动等技术,直接在用户态处理网卡接收和发送的数据包。

  • 常见的用户态协议栈有

    • Ntytcp(自己实现)
    • 4.4BSD
    • mtcp
    • lwip
    • vpp
  • netmap 开源框架:netmap 是一个用于高速数据包处理的开源框架,提供了用户态直接访问网卡的能力,减少了内核干预。它允许应用程序直接操作网卡接收和发送队列,降低了数据传输延迟,提高了网络 I/O 性能,常被用于构建高性能网络应用。

  • eth 协议,ip 协议,udp 协议实现

    • eth 协议(以太网协议):负责在局域网内实现数据帧的传输,定义了数据帧的格式,包括源 MAC 地址、目的 MAC 地址、类型字段等,用于在物理网络中寻址和传输数据。
    • ip 协议(网际协议):是网络层核心协议,提供无连接、不可靠的数据报传输服务。负责根据 IP 地址进行数据包的路由选择,使数据包能在不同网络间传输。
    • udp 协议(用户数据报协议):传输层协议,提供无连接、不可靠的传输服务。它在 IP 协议基础上,增加了端口号来区分不同应用程序,适合对实时性要求高、能容忍少量丢包的应用,如视频流、在线游戏等。
  • arp 协议实现:ARP(Address Resolution Protocol,地址解析协议)用于将 IP 地址解析为对应的 MAC 地址。在局域网中,当主机要向另一台主机发送数据时,需要知道对方的 MAC 地址,ARP 协议通过广播请求、单播应答的方式来获取 MAC 地址。

  • icmp 协议实现:ICMP(Internet Control Message Protocol,网际控制报文协议)主要用于在 IP 主机、路由器之间传递控制消息。如网络不通时的差错报告(目的不可达)、网络时延测量(ping 命令基于此)等,帮助网络管理员诊断和排查网络问题。

主函数与mac、IP头解析
// ----------------------------------------主函数与核心逻辑----------------------------------------------
int main(int argc, char *argv[]) {// 1. 初始化DPDK环境抽象层(EAL)if (rte_eal_init(argc, argv) < 0) {rte_exit(EXIT_FAILURE, "Error with EAL init\n");}// 2. 创建MBUF内存池(用于存储数据包缓冲区)struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool",        // 内存池名称NUM_MBUFS,          // 缓冲区数量0,                  // 每个缓冲区额外数据大小0,                  // 缓存大小RTE_MBUF_DEFAULT_BUF_SIZE, // 缓冲区大小(默认值)rte_socket_id()     // Socket ID(自动分配));if (mbuf_pool == NULL) {rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");}// 3. 初始化网络端口(配置队列、启动设备)ustack_init_port(mbuf_pool);// 4. 主循环:持续接收和处理数据包while (1) {struct rte_mbuf *mbufs[BURST_SIZE] = {0}; // 存储接收的数据包指针// 从接收队列中批量读取数据包(BURST_SIZE=128)uint16_t num_recvd = rte_eth_rx_burst(global_portid,  // 端口ID0,              // 队列IDmbufs,          // 存储指针数组BURST_SIZE      // 最大读取数量);if (num_recvd > BURST_SIZE) {rte_exit(EXIT_FAILURE, "Error receiving from eth\n");}// 遍历每个接收到的数据包for (int i = 0; i < num_recvd; i++) {struct rte_ether_hdr *ethhdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);// 过滤非IPv4数据包if (ethhdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {continue;}// 获取IP头部(偏移以太网头部长度)struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
UPD数据收发实现
UDP数据包构建函数
// ----------------------------UDP 数据包构建函数-----------------------------
// 函数功能:构建UDP响应数据包(以太网层+IP层+UDP层)
static int ustack_encode_udp_pkt(uint8_t *msg, uint8_t *data, uint16_t total_len) {// 1. 构建以太网头部struct rte_ether_hdr *eth = (struct rte_ether_hdr *)msg;// 设置目的MAC地址(从全局变量获取,通常为客户端MAC)rte_memcpy(eth->d_addr.addr_bytes, global_dmac, RTE_ETHER_ADDR_LEN);// 设置源MAC地址(本地网卡MAC)rte_memcpy(eth->s_addr.addr_bytes, global_smac, RTE_ETHER_ADDR_LEN);// 设置以太网类型为IPv4eth->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版本(4位)+ 头部长度(20字节,5*4字节)ip->type_of_service = 0;         // 服务类型(暂不使用)// 总长度:IP包总长度(不含以太网头部)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; // 协议类型:UDPip->src_addr = global_sip;       // 源IP地址(网络字节序)ip->dst_addr = global_dip;       // 目的IP地址(网络字节序)// 计算IP头部校验和(必须在字段设置后计算)ip->hdr_checksum = rte_ipv4_cksum(ip);// 3. 构建UDP头部(紧接在IP头部之后)struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);udp->src_port = global_sport;    // 源端口(网络字节序)udp->dst_port = global_dport;    // 目的端口(网络字节序)// UDP数据长度:总长度 - 以太网头部 - IP头部uint16_t udplen = total_len - sizeof(struct rte_ether_hdr) - sizeof(struct rte_ipv4_hdr);udp->dgram_len = htons(udplen);  // UDP数据报长度(含头部和数据)// 拷贝用户数据到UDP数据部分rte_memcpy((uint8_t*)(udp+1), data, udplen);// 计算UDP校验和(包含IP伪头部)udp->dgram_cksum = rte_ipv4_udptcp_cksum(ip, udp);return 0;
}
  • ip->version_ihl 为什么是0x45

    • 高 4 位:IPv4 版本号:IPv4 的版本号是 4,在二进制中表示为 0100

    • 低 4 位:头部长度:IPv4 头部的基本长度是 20 字节,而低 4 位表示的头部长度是以 32 位(4 字节)为单位的。因此,20 字节的头部长度对应的 32 位字的数量是 20 / 4 = 5,在二进制中表示为 0101

    • 组合 version_ihl 字段:将高 4 位的版本号 0100 和低 4 位的头部长度 0101 组合在一起,得到二进制值 0100 0101。将这个二进制值转换为十六进制,就是 0x45

处理UDP数据包=
 // 处理UDP数据包if (iphdr->next_proto_id == IPPROTO_UDP) {struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);
#if ENABLE_SEND// 提取客户端地址信息(用于响应)rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN); // 客户端MAC作为目的MACrte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN); // 本地MAC作为源MACglobal_sip = iphdr->dst_addr;   // 客户端IP作为源IP(响应时反向)global_dip = iphdr->src_addr;   // 本地IP作为目的IPglobal_sport = udphdr->dst_port; // 客户端端口作为源端口global_dport = udphdr->src_port; // 本地端口作为目的端口// 打印客户端地址和端口struct in_addr addr;addr.s_addr = iphdr->src_addr;printf("sip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->src_port));addr.s_addr = iphdr->dst_addr;printf("dip %s:%d --> ", inet_ntoa(addr), ntohs(udphdr->dst_port));// 构建响应数据包uint16_t length = ntohs(udphdr->dgram_len);uint16_t total_len = length + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);if (!mbuf) {rte_exit(EXIT_FAILURE, "Error allocating mbuf\n");}mbuf->pkt_len = total_len;mbuf->data_len = total_len;uint8_t *msg = rte_pktmbuf_mtod(mbuf, uint8_t *);ustack_encode_udp_pkt(msg, (uint8_t*)(udphdr+1), total_len); // 响应数据为客户端原始数据// 发送响应数据包rte_eth_tx_burst(global_portid, 0, &mbuf, 1);
#endif// 打印UDP数据内容printf("udp : %s\n", (char*)(udphdr+1));}
TCP数据收发实现
TCP数据包构建函数
// --------------------------TCP 数据包构建函数-------------------------
// 函数功能:构建TCP响应数据包(以太网层+IP层+TCP层)
static int ustack_encode_tcp_pkt(uint8_t *msg, uint16_t total_len) {// 1. 以太网头部(与UDP一致)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头部(与UDP一致,协议类型改为TCP)struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr*)(eth + 1);ip->version_ihl = 0x45;ip->type_of_service = 0;ip->total_length = htons(total_len - sizeof(struct rte_ether_hdr));ip->packet_id = 0;ip->fragment_offset = 0;ip->time_to_live = 64;ip->next_proto_id = IPPROTO_TCP; // 协议类型:TCPip->src_addr = global_sip;ip->dst_addr = global_dip;ip->hdr_checksum = 0;ip->hdr_checksum = rte_ipv4_cksum(ip);// 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;                // 数据偏移:20字节头部(0x5*4字节)// 设置TCP标志位:SYN+ACK(表示响应连接请求)tcp->tcp_flags = RTE_TCP_SYN_FLAG | RTE_TCP_ACK_FLAG;tcp->rx_win = htons(TCP_INIT_WINDOWS); // 接收窗口大小(流量控制)// 计算TCP校验和(包含IP伪头部)tcp->cksum = rte_ipv4_udptcp_cksum(ip, tcp);return 0;
}
  • UDP 有长度字段原因:UDP 头部包含一个长度字段,占 16 位。它记录的是 UDP 头部加上 UDP 数据部分的总长度。这是因为 UDP 是无连接、不可靠的传输协议 ,没有像 TCP 那样复杂的连接管理和数据传输机制。接收方需要通过这个长度字段来准确知道从 IP 数据报中提取出多长的数据作为 UDP 数据,从而正确处理数据。
  • TCP 没有长度字段原因:TCP 虽然头部没有专门的长度字段,但可以通过 IP 头部的总长度字段和 IP 头部长度字段间接得到 TCP 数据的长度。因为 TCP 是基于连接的可靠传输协议,它与 IP 层紧密配合。在 IP 数据报中,IP 头部的总长度字段表示整个 IP 数据报(包含 IP 头部、TCP 头部和 TCP 数据)的长度,IP 头部长度字段表示 IP 头部自身的长度。通过两者相减,就能得到 TCP 头部和 TCP 数据的总长度,再结合 TCP 头部的固定长度(20 字节,无选项时),就可以准确分离出 TCP 数据。此外,TCP 通过序列号、确认号等机制来管理数据的有序传输和接收,不需要像 UDP 那样在头部专门设置长度字段来界定数据边界。
TCP数据包构建函数
// 处理TCP数据包else if (iphdr->next_proto_id == IPPROTO_TCP) {struct rte_tcp_hdr *tcphdr = (struct rte_tcp_hdr *)(iphdr + 1);// 提取客户端地址信息(与UDP类似)rte_memcpy(global_smac, ethhdr->d_addr.addr_bytes, RTE_ETHER_ADDR_LEN);rte_memcpy(global_dmac, ethhdr->s_addr.addr_bytes, RTE_ETHER_ADDR_LEN);global_sip = iphdr->dst_addr;global_dip = iphdr->src_addr;global_sport = tcphdr->dst_port;global_dport = tcphdr->src_port;// 提取TCP标志位和序列号global_flags = tcphdr->tcp_flags;global_seqnum = ntohl(tcphdr->sent_seq); // 网络字节序转主机字节序global_acknum = ntohl(tcphdr->recv_ack);// 打印TCP数据包信息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: 0x%x, seq: %u, ack: %u\n", inet_ntoa(addr), ntohs(tcphdr->dst_port), global_flags, global_seqnum, global_acknum);// 处理TCP连接请求(SYN标志)if (global_flags & RTE_TCP_SYN_FLAG) {if (tcp_status == USTACK_TCP_STATUS_LISTEN) {// 构建SYN+ACK响应包(三次握手第二步)uint16_t total_len = sizeof(struct rte_tcp_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool);if (!mbuf) {rte_exit(EXIT_FAILURE, "Error allocating 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包构建函数rte_eth_tx_burst(global_portid, 0, &mbuf, 1); // 发送响应tcp_status = USTACK_TCP_STATUS_SYN_RCVD; // 更新连接状态}}// 处理ACK标志(完成三次握手)if (global_flags & RTE_TCP_ACK_FLAG) {if (tcp_status == USTACK_TCP_STATUS_SYN_RCVD) {printf("enter established\n");tcp_status = USTACK_TCP_STATUS_ESTABLISHED; // 连接建立完成}}// 处理PSH标志(有数据需要传输)if (global_flags & RTE_TCP_PSH_FLAG) {if (tcp_status == USTACK_TCP_STATUS_ESTABLISHED) {// 解析TCP数据部分(跳过头部)uint8_t hdrlen = (tcphdr->data_off >> 4) * sizeof(uint32_t); // 头部长度(4字节为单位)uint8_t *data = (uint8_t*)tcphdr + hdrlen;printf("tcp data: %s\n", data); // 打印数据内容}}}

2.4.2 tcp 的原理实现

tcp三次握手的实现
tcp状态机

在这里插入图片描述

  • tcp 11 个状态实现:TCP(Transmission Control Protocol,传输控制协议)是面向连接、可靠的传输层协议,有 11 种状态,如 CLOSED(关闭)、LISTEN(监听)、SYN_SENT(同步已发送)、SYN_RECV(同步接收 )、ESTABLISHED(已建立连接)等。这些状态描述了 TCP 连接从建立、数据传输到断开的整个过程 。

    // ------------------------全局变量定义---------------------------
    #if ENABLE_TCP
    // TCP协议相关全局变量
    uint8_t global_flags;    // TCP标志位(SYN/ACK/PSH等)
    uint32_t global_seqnum;  // 序列号
    uint32_t global_acknum;  // 确认号// TCP状态枚举:定义TCP连接的11种状态(如监听、连接建立、关闭等)
    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,     // 第一次FIN等待(主动关闭连接)USTACK_TCP_STATUS_FIN_WAIT_2,     // 第二次FIN等待USTACK_TCP_STATUS_CLOSING,        // 双向关闭中USTACK_TCP_STATUS_TIMEWAIT,       // 超时等待(确保最后一个ACK到达)USTACK_TCP_STATUS_CLOSE_WAIT,     // 等待关闭(被动关闭连接)USTACK_TCP_STATUS_LAST_ACK        // 最后确认状态
    } USTACK_TCP_STATUS;// 全局TCP状态变量:初始化为监听状态,等待客户端连接
    uint8_t tcp_status = USTACK_TCP_STATUS_LISTEN;
    #endif
    

    在状态机判断中:

    // 处理TCP连接请求(SYN标志)if (global_flags & RTE_TCP_SYN_FLAG) {if (tcp_status == USTACK_TCP_STATUS_LISTEN) {// 构建SYN+ACK响应包(三次握手第二步)uint16_t total_len = sizeof(struct rte_tcp_hdr) + sizeof(struct rte_ipv4_hdr) + sizeof(struct rte_ether_hdr);struct rte_mbuf *mbuf = rte_pktmbuf_alloc(mbuf_pool); if (!mbuf) {rte_exit(EXIT_FAILURE, "Error allocating 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包构建函数rte_eth_tx_burst(global_portid, 0, &mbuf, 1); // 发送响应tcp_status = USTACK_TCP_STATUS_SYN_RCVD; // 更新连接状态}}// 处理ACK标志(完成三次握手)if (global_flags & RTE_TCP_ACK_FLAG) {if (tcp_status == USTACK_TCP_STATUS_SYN_RCVD) {printf("enter established\n");tcp_status = USTACK_TCP_STATUS_ESTABLISHED; // 连接建立完成}}// 处理PSH标志(有数据需要传输)if (global_flags & RTE_TCP_PSH_FLAG) {if (tcp_status == USTACK_TCP_STATUS_ESTABLISHED) {// 解析TCP数据部分(跳过头部)uint8_t hdrlen = (tcphdr->data_off >> 4) * sizeof(uint32_t); // 头部长度(4字节为单位)uint8_t *data = (uint8_t*)tcphdr + hdrlen;printf("tcp data: %s\n", data); // 打印数据内容}}
    
tcp传输过程的原理
滑动窗口
  • 滑动窗口

    • 滑动窗口机制在 TCP 中起到了至关重要的流量控制作用。发送方和接收方都各自维护一个窗口。发送方的窗口决定了在没有收到接收方确认信息的情况下,能够连续发送的数据量。这个窗口大小是动态变化的,它会根据接收方的反馈进行调整。
    • 接收方会在发送的 ACK 报文中告知发送方自己当前可用的接收缓冲区大小,也就是窗口大小。发送方会根据这个信息调整自己的发送速率。例如,如果接收方的窗口大小为 0,发送方就会暂停发送数据,直到收到接收方更新的窗口大小信息。
    • 发送方的窗口可以分为三个部分:已发送已确认、已发送未确认和未发送未确认。已发送已确认部分表示数据已经成功发送并且得到了接收方的确认;已发送未确认部分表示数据已经发送出去,但还没有收到接收方的确认;未发送未确认部分表示还没有发送的数据。随着数据的发送和确认,窗口会不断地 “滑动”,这也是滑动窗口名称的由来。
  • 延迟确认

    • 接收方在收到数据后,并不一定会立即发送 ACK 确认报文。延迟确认机制允许接收方等待一段时间,看是否还有后续的数据到达。如果在这段时间内又收到了新的数据,接收方可以将多个数据段的确认合并成一个 ACK 报文发送,这样可以减少网络中的报文数量,提高传输效率。
    • 不过,延迟确认也有一定的时间限制,以确保发送方不会长时间等待确认信息。如果在规定时间内没有新的数据到达,接收方也会发送 ACK 报文进行确认。
慢启动
  • 慢启动

    • 慢启动是 TCP 拥塞控制机制的重要组成部分。在 TCP 连接刚刚建立时,网络的拥塞情况是未知的。为了避免一开始就发送大量的数据导致网络拥塞,发送方会以一个较小的拥塞窗口开始发送数据。通常,这个初始拥塞窗口的大小为一个 MSS(最大报文段长度)。
    • 每收到一个 ACK 确认,发送方的拥塞窗口就会增加一个 MSS。这样,随着时间的推移,发送方的发送速率会逐渐增加。例如,在第一个 RTT(往返时间)内,发送方发送一个 MSS 的数据,收到 ACK 后,拥塞窗口变为 2 个 MSS;在第二个 RTT 内,发送方可以发送 2 个 MSS 的数据,收到 ACK 后,拥塞窗口变为 4 个 MSS,以此类推。
    • 慢启动的目的是通过逐步增加发送速率,来探测网络的拥塞情况。当拥塞窗口达到一定阈值(慢启动阈值)时,慢启动过程结束,进入拥塞避免阶段。
拥塞控制
  • 往返时间 RTT:
    • RTT 是指从发送方发送数据开始,到发送方收到接收方的 ACK 确认所经历的时间。它是 TCP 拥塞控制和流量控制的重要参数。
    • 发送方可以通过测量 RTT 来估计网络的延迟情况。如果 RTT 变长,可能表示网络出现了拥塞。发送方会根据 RTT 的变化来调整自己的拥塞窗口大小。例如,当 RTT 明显增加时,发送方会认为网络拥塞加剧,从而减小拥塞窗口,降低发送速率。
    • 为了准确测量 RTT,TCP 通常会使用加权平均的方法。每次测量到一个新的 RTT 值后,会将其与之前的平均 RTT 值进行加权平均,得到一个更平滑的 RTT 估计值。这样可以减少 RTT 测量值的波动对拥塞控制的影响。
超时重传
  • 重传定时器:
    • 重传定时器是 TCP 保证可靠传输的关键机制之一。当发送方发送一个数据段后,会启动一个重传定时器。如果在定时器超时之前没有收到接收方的 ACK 确认,发送方会认为该数据段可能已经丢失,会重新发送该数据段。
    • 重传定时器的超时时间是动态调整的。它通常与 RTT 相关,会根据测量到的 RTT 值进行调整。如果网络延迟较大,RTT 较长,重传定时器的超时时间也会相应地增加;反之,如果网络延迟较小,RTT 较短,重传定时器的超时时间也会缩短。
  • 坚持定时器:
    • 坚持定时器主要用于解决零窗口通知问题。当接收方的缓冲区已满时,会发送一个窗口大小为 0 的 ACK 报文给发送方,告知发送方停止发送数据。此时,发送方会暂停发送数据,等待接收方的窗口更新。
    • 为了防止接收方后续有了空闲缓冲区,但由于窗口更新报文丢失,导致发送方一直等待的情况,发送方会设置一个坚持定时器。当坚持定时器超时后,发送方会发送一个探测报文,询问接收方的窗口状态。如果接收方有了空闲缓冲区,会在响应报文中告知发送方新的窗口大小,发送方可以根据这个信息继续发送数据。
其他定时器
  • time_wait 定时器:
    • 在 TCP 连接关闭过程中,主动关闭连接的一方会进入 TIME_WAIT 状态,并启动 time_wait 定时器。该定时器会等待 2 倍的 MSL(报文段最大生存时间)时长。
    • 等待 2 倍 MSL 时长的目的是确保网络中所有与该连接相关的旧数据包都已消失。如果没有这个等待时间,当新的连接建立时,可能会收到旧连接的残留数据包,导致数据混乱。此外,等待 2 倍 MSL 时长还可以保证最后一个 ACK 报文有足够的时间到达对端,避免对端因未收到 ACK 而重发 FIN 报文。
  • keepalive 定时器:
    • keepalive 定时器用于检测 TCP 连接是否仍然有效。在连接处于空闲状态(即一段时间内没有数据传输)时,发送方会定时发送探测报文给接收方。
    • 如果接收方在规定时间内没有响应,发送方会继续发送探测报文。经过一定次数的重试后,如果仍然没有收到接收方的响应,发送方会认为连接已经断开,并关闭连接。这样可以防止出现死连接,释放系统资源。

2.4.3 应用层 posix api 的具体实现

第一层网卡到内核loop循环的实现

在这里插入图片描述

	while (1) {// 接收数据包struct rte_mbuf *rx[BURST_SIZE];unsigned num_recvd = rte_eth_rx_burst(gDpdkPortId, 0, rx, BURST_SIZE);if (num_recvd > BURST_SIZE) {rte_exit(EXIT_FAILURE, "Error receiving from eth\n");} else if (num_recvd > 0) {
#if ENABLE_DDOS_DETECTunsigned i = 0;for (i = 0;i < num_recvd;i ++) {// 进行DDoS检测ddos_detect(rx[i]);}
#endif// 将接收到的数据包放入输入环形缓冲区rte_ring_sp_enqueue_burst(ring->in, (void**)rx, num_recvd, NULL);}// 发送数据包struct rte_mbuf *tx[BURST_SIZE];unsigned nb_tx = rte_ring_sc_dequeue_burst(ring->out, (void**)tx, BURST_SIZE, NULL);if (nb_tx > 0) {// 批量发送数据包rte_eth_tx_burst(gDpdkPortId, 0, tx, nb_tx);unsigned i = 0;for (i = 0;i < nb_tx;i ++) {// 释放mbufrte_pktmbuf_free(tx[i]);}}}
第二层tcp/ip协议栈循环的实现

在这里插入图片描述

// 定义一个名为pkt_process的函数,该函数用于处理数据包,参数arg是一个void指针,通常用于传递所需的数据结构
static int pkt_process(void *arg) {// 将传入的参数arg转换为rte_mempool类型的指针,该内存池用于分配数据包缓冲区struct rte_mempool *mbuf_pool = (struct rte_mempool *)arg;// 获取输入输出环形缓冲区的实例,该环形缓冲区用于数据包的输入和输出struct inout_ring *ring = ringInstance();// 进入一个无限循环,持续处理数据包while (1) {// 定义一个rte_mbuf类型的数组,用于存储从环形缓冲区中取出的数据包,BURST_SIZE表示一次处理的最大数据包数量struct rte_mbuf *mbufs[BURST_SIZE];// 从输入环形缓冲区中批量取出数据包,num_recvd表示实际取出的数据包数量unsigned num_recvd = rte_ring_mc_dequeue_burst(ring->in, (void**)mbufs, BURST_SIZE, NULL);// 遍历取出的所有数据包unsigned i = 0;for (i = 0;i < num_recvd;i ++) {// 从数据包缓冲区中获取以太网头部信息struct rte_ether_hdr *ehdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr*);// 检查以太网帧的类型是否为IPv4if (ehdr->ether_type == rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {// 从数据包缓冲区中偏移以太网头部大小的位置获取IPv4头部信息struct rte_ipv4_hdr *iphdr =  rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));#if 1 // arp table// 如果启用了ARP表功能,将源IP地址和对应的MAC地址插入到ARP表中ng_arp_entry_insert(iphdr->src_addr, ehdr->s_addr.addr_bytes);#endif// 检查IPv4数据包的下一层协议是否为UDPif (iphdr->next_proto_id == IPPROTO_UDP) {// 调用udp_process函数处理UDP数据包udp_process(mbufs[i]);// 注释表明此处可能处理端口号为53的数据包,但代码中未实现// 53 --> // // 检查IPv4数据包的下一层协议是否为TCP} else if (iphdr->next_proto_id == IPPROTO_TCP) {// 调用ng_tcp_process函数处理TCP数据包ng_tcp_process(mbufs[i]);// 如果既不是UDP也不是TCP协议} else {// 将数据包通过KNI(Kernel Network Interface)接口发送到内核空间进行处理rte_kni_tx_burst(global_kni, mbufs, num_recvd);// 注释表明此处可能会打印信息,但代码中被注释掉了//printf("tcp/udp --> rte_kni_handle_request\n");}// 如果以太网帧的类型不是IPv4} else {// 注释表明可能需要执行的操作,如配置虚拟以太网接口// ifconfig vEth0 192.168.0.119 up// 将数据包通过KNI接口发送到内核空间进行处理rte_kni_tx_burst(global_kni, mbufs, num_recvd);// 注释表明此处可能会打印信息,但代码中被注释掉了//printf("ip --> rte_kni_handle_request\n");}}// 处理KNI接口的请求,确保KNI接口正常工作rte_kni_handle_request(global_kni);#if ENABLE_UDP_APP// 如果启用了UDP应用程序功能,调用udp_out函数处理UDP输出数据包udp_out(mbuf_pool);#endif#if ENABLE_TCP_APP// 如果启用了TCP应用程序功能,调用ng_tcp_out函数处理TCP输出数据包ng_tcp_out(mbuf_pool);#endif}// 函数正常结束,返回0表示成功return 0;
}
recv/send 的实现
  • rcvbuf与sndbuf是一个连接独享的,不是所有连接共用的
struct ng_tcp_stream { // tcb control blockint fd; //uint32_t dip;uint8_t localmac[RTE_ETHER_ADDR_LEN];uint16_t dport;uint8_t protocol;uint16_t sport;uint32_t sip;uint32_t snd_nxt; // seqnumuint32_t rcv_nxt; // acknumNG_TCP_STATUS status;
#if 0union {struct {struct ng_tcp_stream *syn_set; //struct ng_tcp_stream *accept_set; //};struct {struct rte_ring *sndbuf;struct rte_ring *rcvbuf;};};
#elsestruct rte_ring *sndbuf;struct rte_ring *rcvbuf;
#endifstruct ng_tcp_stream *prev;struct ng_tcp_stream *next;pthread_cond_t cond;pthread_mutex_t mutex;};
  • socket/bind/listen 的实现
    • socket:是应用程序与网络协议栈交互的接口,用于创建一个通信端点,指定协议族(如 IPv4、IPv6)、套接字类型(如流式套接字 SOCK_STREAM 用于 TCP,数据报套接字 SOCK_DGRAM 用于 UDP )和协议(如 TCP、UDP )。
    • bind:将套接字与特定的地址和端口绑定,使该套接字能够接收发往此地址和端口的数据。对于服务器端,需要明确绑定监听的 IP 地址和端口号,以便客户端能连接到它。
    • listen:用于将套接字设置为监听模式,仅适用于流式套接字(TCP)。它会创建一个请求队列,用于存放客户端的连接请求,等待服务器后续处理。
  • accept 实现:在服务器端,当有客户端发起连接请求并放入请求队列后,accept 函数用于从请求队列中取出一个连接请求,并创建一个新的套接字用于与该客户端进行通信,实现服务器与客户端的连接建立。
  • recv/send 的实现
    • recv:用于从套接字接收数据,应用程序调用该函数,指定接收数据的缓冲区、缓冲区大小等参数,从网络中接收数据并存储到缓冲区中。
    • send:用于向套接字发送数据,应用程序将待发送的数据缓冲区和相关参数传递给 send 函数,通过网络将数据发送给对方。

2.4.4 手把手设计实现 epoll

关键的四个问题:
enum EPOLL_EVENTS {EPOLLNONE 	= 0x0000,EPOLLIN 	= 0x0001,EPOLLPRI	= 0x0002,EPOLLOUT	= 0x0004,EPOLLRDNORM = 0x0040,EPOLLRDBAND = 0x0080,EPOLLWRNORM = 0x0100,EPOLLWRBAND = 0x0200,EPOLLMSG	= 0x0400,EPOLLERR	= 0x0008,EPOLLHUP 	= 0x0010,EPOLLRDHUP 	= 0x2000,EPOLLONESHOT = (1 << 30),EPOLLET 	= (1 << 31)};#define EPOLL_CTL_ADD	1
#define EPOLL_CTL_DEL	2
#define EPOLL_CTL_MOD	3typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events;epoll_data_t data;
};struct epitem {RB_ENTRY(epitem) rbn;LIST_ENTRY(epitem) rdlink;int rdy; //exist in list int sockfd;struct epoll_event event; 
};static int sockfd_cmp(struct epitem *ep1, struct epitem *ep2) {if (ep1->sockfd < ep2->sockfd) return -1;else if (ep1->sockfd == ep2->sockfd) return 0;return 1;
}RB_HEAD(_epoll_rb_socket, epitem);
RB_GENERATE_STATIC(_epoll_rb_socket, epitem, rbn, sockfd_cmp);typedef struct _epoll_rb_socket ep_rb_tree;struct eventpoll {ep_rb_tree rbr;int rbcnt;LIST_HEAD( ,epitem) rdlist;int rdnum;int waiting;pthread_mutex_t mtx; //rbtree updatepthread_spinlock_t lock; //rdlist updatepthread_cond_t cond; //block for eventpthread_mutex_t cdmtx; //mutex for cond};int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event);
int nepoll_create(int size);
int nepoll_ctl(int epfd, int op, int sockid, struct epoll_event *event);
int nepoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 红黑树的数据结构是什么?

  • epoll_create(创建一个 epoll 实例)、epoll_ctl(添加、修改或删除监控的文件描述符)、epoll_wait(等待并获取就绪事件)接口的实现?

  • 协议栈如何通知到epoll?

  • 如何从整集到单个?

epoll 数据结构封装与线程安全实现——rbtree 对<fd, event>的存储,ready 队列存储就绪 io
  • epoll 是 Linux 下的一种高效 I/O 多路复用机制。epoll 数据结构用于管理关注的文件描述符集合和就绪事件列表。对其进行封装是为了更方便地使用和管理,同时考虑多线程环境下的访问,通过锁机制等手段保证线程安全,防止多个线程同时操作 epoll 数据结构时出现竞态条件。

  • epoll为什么选择红黑树?为什么不用hash、b树、跳表?

    • 在 epoll 这种需要处理大量且数量不确定的 I/O 描述符的场景中,数据结构的选择确实需要综合考虑查找效率、内存增长特性以及树的层级等多方面因素。下面从这几个关键点进一步解释为何红黑树是更优选择:

      1. 哈希表的内存增长问题
      • 非线性扩容开销:哈希表通常采用链地址法或开放寻址法处理冲突,当元素数量超过负载因子时需要扩容(例如将数组大小翻倍)。这种扩容操作是非线性的,需要重新计算所有元素的哈希值并插入新表,在高并发场景下可能导致瞬间性能抖动。
      • 内存碎片化:频繁扩容会导致内存碎片化,尤其在管理大量文件描述符时,这种内存浪费可能变得不可接受。
      • 对比红黑树:红黑树的插入 / 删除操作时间复杂度始终为 O (log n),内存增长是线性且可预测的,每次插入仅增加一个节点,无需批量调整。
      1. B 树的层级与内存使用
      • 层级优势与场景不匹配:B 树(如 B + 树)的层级确实比红黑树更低(例如,100 万个节点的 B 树可能只需 3-4 层),但这一优势主要体现在磁盘 I/O 优化上(减少磁盘寻道次数)。而 epoll 的操作完全在内存中进行,层级带来的优势被削弱。
      • 节点冗余开销:B 树的每个节点需要存储多个键值对和子节点指针(例如,一个节点可能存储 100 + 个键),这会导致额外的内存开销。对于内存中的高频操作,这种冗余不如红黑树紧凑。
      • 动态调整成本:B 树的插入 / 删除需要维护节点的平衡(如分裂 / 合并操作),虽然层级低,但单次操作的常数时间可能更高,不如红黑树轻量。
      1. 红黑树的内存无需连续内存
      • 确定性性能:红黑树的高度始终保持在 O (log n),确保了插入、删除和查找操作的最坏时间复杂度均为 O (log n),不存在哈希表的扩容抖动或 B 树的节点分裂开销。
      • 内存效率:红黑树每个节点仅需额外 1 个字节存储颜色信息,且无需预分配大量连续内存(如哈希表的数组),更适合动态增长的场景。
      • 范围查询支持:epoll 需要快速获取所有就绪的描述符(范围查询),红黑树通过中序遍历可高效实现,而哈希表和 B 树在这方面的表现较差。
  • Epoll 主要由两个结构体:eventpoll 与 epitem。Epitem 是每一个 IO 所对应的的事件。比如epoll_ctl EPOLL_CTL_ADD 操作的时候,就需要创建一个 epitem。Eventpoll 是每一个 epoll 所对应的整体。比如 epoll_create 就是创建一个 eventpoll。

  • 在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • List 用来存储准备就绪的 IO。对于数据结构主要讨论两方面:insert 与 remove。同样如此,对于 list 我们也讨论 insert 与 remove。何时将数据插入到 list 中呢?当内核 IO 准备就绪的时候,则会执行 epoll_event_callback 的回调函数,将 epitem 添加到 list 中。

    • 那何时删除 list 中的数据呢?当 epoll_wait 激活重新运行的时候,将 list 的 epitem 逐一 copy到 events 参数中。
  • Epoll 从以下几个方面是需要加锁保护的。List 的操作,rbtree 的操作,epoll_wait 的等待。

    • List 使用最小粒度的锁 spinlock,便于在 SMP 下添加操作的时候,能够快速操作 li

    • 在这里插入图片描述

    • List 删除

在这里插入图片描述

协议栈 fd 就绪回调实现
  • 当 epoll 所监控的文件描述符(fd)上有事件发生(如可读、可写等)时,epoll 会将其加入就绪事件列表。协议栈 fd 就绪回调实现,就是定义当这些文件描述符就绪时,相应的处理逻辑,比如从套接字接收数据或向套接字发送数据。

  • 在这里插入图片描述

  • Epoll 的回调函数何时执行,此部分需要与Tcp的协议栈一起来阐述。Tcp协议栈的时序图如下图所示,epoll 从协议栈回调的部分从下图的编号1,2,3,4。具体Tcp协议栈的实现,后续从另外的文章中表述出来。下面分别对四个步骤详细描述

    • 编号1:是tcp三次握手,对端反馈ack后,socket进入rcvd 状态。需要将监听 socket 的event 置为EPOLLIN,此时标识可以进入到accept 读取socket 数据。

    • 编号 2:在established 状态,收到数据以后,需要将socket的event 置为EPOLLIN 状态。

    • 编号3: 在 established 状态,收到fin 时,此时 socket 进入到 close_wait。需要socket 的 event置为 EPOLLIN。读取断开信息。

    • 编号 4:检测socket的send 状态,如果对端cwnd>0是可以,发送的数据。故需要将 socket置为 EPOLLOUT。

  • 所以在此四处添加EPOLL的回调函数,即可使得epoll正常接收到io事件。

  • 代码实现:

  • /*** 事件回调函数:当套接字有事件发生时被调用* @param ep      所属的 eventpoll 实例* @param sockid  发生事件的套接字描述符* @param event   发生的事件类型(如 EPOLLIN、EPOLLOUT)* @return 成功返回 0,已就绪返回 1,失败返回 -1*/
    int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {// 在红黑树中查找目标套接字对应的 epitemstruct epitem tmp;tmp.sockfd = sockid;struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);if (!epi) {printf("rbtree not exist\n");return -1;  // 未找到对应项,返回错误}// 如果该描述符已在就绪队列中,直接更新事件掩码if (epi->rdy) {epi->event.events |= event;return 1;  // 已在就绪队列,仅更新事件} printf("epoll_event_callback --> %d\n", epi->sockfd);// 使用自旋锁保护就绪队列操作(快速操作)pthread_spin_lock(&ep->lock);// 标记为就绪并加入就绪链表epi->rdy = 1;LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);  // 头插法保证优先级ep->rdnum++;  // 增加就绪计数pthread_spin_unlock(&ep->lock);// 使用互斥锁保护条件变量操作pthread_mutex_lock(&ep->cdmtx);// 唤醒可能正在等待的 epoll_wait 线程pthread_cond_signal(&ep->cond);pthread_mutex_unlock(&ep->cdmtx);return 0;  // 操作成功
    }
    

    这段代码实现了 epoll 机制的事件通知核心逻辑:

    1. 查找事件项
      • 通过红黑树快速查找目标套接字对应的 epitem
      • 若未找到,记录错误并返回
    2. 处理已就绪事件
      • 若描述符已在就绪队列中(rdy == 1),仅更新事件掩码
      • 避免重复添加到就绪队列,实现边缘触发(EPOLLET)语义
    3. 添加到就绪队列
      • 使用自旋锁 ep->lock 保护就绪队列操作
      • 将事件项标记为就绪(rdy = 1
      • 使用链表头插法(LIST_INSERT_HEAD)添加到就绪队列
      • 更新就绪计数 rdnum
    4. 唤醒等待线程
      • 使用互斥锁 ep->cdmtx 保护条件变量
      • 发送信号(pthread_cond_signal)唤醒可能正在等待的 epoll_wait
      • 遵循 “锁 - 条件变量” 的标准使用模式
epoll 接口实现
  • 实现 epoll 相关接口,如 epoll_create(创建一个 epoll 实例)、epoll_ctl(添加、修改或删除监控的文件描述符)、epoll_wait(等待并获取就绪事件)等,使应用程序能够方便地使用 epoll 机制来管理多个 I/O 操作,提高 I/O 处理效率。
epoll_create
/*** 创建一个epoll实例,返回文件描述符* @param size 此参数被忽略,但必须大于0(兼容Linux epoll接口)* @return 成功时返回epoll实例的文件描述符,失败时返回-1或-2*/
int nepoll_create(int size) {// 检查size参数有效性(Linux epoll要求size>0,但实际被忽略)if (size <= 0) return -1;// 从位图中分配一个文件描述符(类似TCP/UDP的fd分配)int epfd = get_fd_frombitmap();// 分配eventpoll结构体内存struct eventpoll *ep = (struct eventpoll*)rte_malloc("eventpoll", sizeof(struct eventpoll), 0);if (!ep) {// 内存分配失败,释放已分配的fdset_fd_frombitmap(epfd);return -1;}// 将此epoll实例关联到全局TCP表(假设所有TCP连接共享此epoll)struct ng_tcp_table *table = tcpInstance();table->ep = ep;// 初始化eventpoll结构体ep->fd = epfd;                 // 存储文件描述符ep->rbcnt = 0;                 // 红黑树节点计数RB_INIT(&ep->rbr);             // 初始化红黑树(用于存储注册的文件描述符)LIST_INIT(&ep->rdlist);        // 初始化就绪链表(存储就绪的文件描述符)// 初始化各种锁和条件变量// 1. 主互斥锁(保护epoll内部数据结构)if (pthread_mutex_init(&ep->mtx, NULL)) {free(ep);set_fd_frombitmap(epfd);return -2;}// 2. 条件变量关联的互斥锁if (pthread_mutex_init(&ep->cdmtx, NULL)) {pthread_mutex_destroy(&ep->mtx);free(ep);set_fd_frombitmap(epfd);return -2;}// 3. 条件变量(用于epoll_wait阻塞唤醒)if (pthread_cond_init(&ep->cond, NULL)) {pthread_mutex_destroy(&ep->cdmtx);pthread_mutex_destroy(&ep->mtx);free(ep);set_fd_frombitmap(epfd);return -2;}// 4. 自旋锁(用于快速保护临界区)if (pthread_spin_init(&ep->lock, PTHREAD_PROCESS_SHARED)) {pthread_cond_destroy(&ep->cond);pthread_mutex_destroy(&ep->cdmtx);pthread_mutex_destroy(&ep->mtx);free(ep);set_fd_frombitmap(epfd);return -2;}// 成功返回epoll实例的文件描述符return epfd;
}

实现解释:

这段代码实现了一个类似 Linux epoll 的 I/O 多路复用机制的初始化函数:

  1. 参数检查
    • 要求size > 0(兼容 Linux epoll 接口,但实际被忽略)
  2. 资源分配
    • 分配文件描述符(epfd
    • 使用rte_malloc分配eventpoll结构体(通常用于 DPDK 环境)
  3. 数据结构初始化
    • 红黑树(rbr):用于高效存储和查找注册的文件描述符
    • 就绪链表(rdlist):用于快速获取就绪的事件
  4. 并发控制初始化
    • 互斥锁(mtx):保护 epoll 内部数据结构
    • 条件变量(cond)及关联锁(cdmtx):用于实现epoll_wait的阻塞和唤醒机制
    • 自旋锁(lock):用于轻量级保护临界区(适合多核环境)
  5. 错误处理
    • 任何资源分配或初始化失败时,会回滚已完成的操作(释放内存、销毁已初始化的锁等)
epoll_ctl
/*** 控制 epoll 实例中的文件描述符监视* @param epfd    epoll 实例的文件描述符* @param op      操作类型(EPOLL_CTL_ADD/DEL/MOD)* @param sockid  要操作的套接字文件描述符* @param event   事件结构(添加/修改时使用)* @return 成功返回 0,失败返回 -1 并设置错误码*/
int nepoll_ctl(int epfd, int op, int sockid, struct epoll_event *event) {// 通过 epfd 获取 eventpoll 结构体struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);// 验证参数有效性:epfd 必须有效,且非删除操作时 event 不能为 NULLif (!ep || (!event && op != EPOLL_CTL_DEL)) {errno = -EINVAL;return -1;}// 添加操作:注册新的文件描述符if (op == EPOLL_CTL_ADD) {pthread_mutex_lock(&ep->mtx);  // 加锁保护红黑树操作// 检查 sockid 是否已注册struct epitem tmp;tmp.sockfd = sockid;struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);if (epi) {  // 已注册则返回错误pthread_mutex_unlock(&ep->mtx);return -1;}// 分配新的 epitem 并初始化epi = (struct epitem*)rte_malloc("epitem", sizeof(struct epitem), 0);if (!epi) {pthread_mutex_unlock(&ep->mtx);rte_errno = -ENOMEM;return -1;}epi->sockfd = sockid;memcpy(&epi->event, event, sizeof(struct epoll_event));// 插入红黑树(RB_INSERT 可能返回 NULL 或已存在的节点,这里假设实现正确)epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);ep->rbcnt++;  // 增加节点计数pthread_mutex_unlock(&ep->mtx);// 删除操作:移除已注册的文件描述符} else if (op == EPOLL_CTL_DEL) {pthread_mutex_lock(&ep->mtx);// 查找要删除的节点struct epitem tmp;tmp.sockfd = sockid;struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);if (!epi) {pthread_mutex_unlock(&ep->mtx);return -1;}// 从红黑树中移除并释放内存epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);if (!epi) {  // 理论上不会执行,仅作防御性编程pthread_mutex_unlock(&ep->mtx);return -1;}ep->rbcnt--;free(epi);pthread_mutex_unlock(&ep->mtx);// 修改操作:更新已注册描述符的事件掩码} else if (op == EPOLL_CTL_MOD) {// 查找要修改的节点struct epitem tmp;tmp.sockfd = sockid;struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);if (epi) {// 更新事件掩码并强制包含错误事件epi->event.events = event->events;epi->event.events |= EPOLLERR | EPOLLHUP;} else {rte_errno = -ENOENT;return -1;}} return 0;  // 操作成功
}

这段代码实现了 epoll 机制的核心控制逻辑:

  1. 参数验证
    • 通过 epfd 获取 eventpoll 结构体
    • 检查操作类型和参数有效性(如非删除操作必须提供 event
  2. 添加操作(EPOLL_CTL_ADD)
    • 加锁保护红黑树操作
    • 检查目标 sockid 是否已注册
    • 分配新的 epitem 节点并初始化
    • 将节点插入红黑树
    • 更新节点计数
  3. 删除操作(EPOLL_CTL_DEL)
    • 加锁保护红黑树操作
    • 查找目标节点并从红黑树移除
    • 释放节点内存
    • 更新节点计数
  4. 修改操作(EPOLL_CTL_MOD)
    • 查找目标节点
    • 更新事件掩码(强制包含错误事件检测)
epoll_wait
/*** 等待 epoll 实例中的事件发生* @param epfd      epoll 实例的文件描述符* @param events    用于存储就绪事件的数组* @param maxevents 最大返回的事件数量* @param timeout   超时时间(毫秒):-1 永久等待,0 立即返回,>0 等待指定毫秒* @return 成功返回就绪事件数量,失败返回 -1 并设置错误码*/
int nepoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) {// 通过 epfd 获取 eventpoll 结构体并验证参数有效性struct eventpoll *ep = (struct eventpoll*)get_hostinfo_fromfd(epfd);if (!ep || !events || maxevents <= 0) {rte_errno = -EINVAL;return -1;}// 获取条件变量关联的互斥锁(保护就绪队列操作)if (pthread_mutex_lock(&ep->cdmtx)) {if (rte_errno == EDEADLK) {printf("epoll lock blocked\n");}return -1;}// 当没有就绪事件且超时时间未到,进入等待状态while (ep->rdnum == 0 && timeout != 0) {ep->waiting = 1;  // 标记当前有线程在等待if (timeout > 0) {  // 有超时的等待struct timespec deadline;clock_gettime(CLOCK_REALTIME, &deadline);// 计算绝对超时时间if (timeout >= 1000) {int sec = timeout / 1000;deadline.tv_sec += sec;timeout -= sec * 1000;}deadline.tv_nsec += timeout * 1000000;// 处理纳秒溢出if (deadline.tv_nsec >= 1000000000) {deadline.tv_sec++;deadline.tv_nsec -= 1000000000;}// 带超时的条件等待int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);if (ret && ret != ETIMEDOUT) {printf("pthread_cond_timewait error\n");pthread_mutex_unlock(&ep->cdmtx);return -1;}timeout = 0;  // 无论是否超时,都结束循环} else if (timeout < 0) {  // 永久等待int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);if (ret) {printf("pthread_cond_wait error\n");pthread_mutex_unlock(&ep->cdmtx);return -1;}}ep->waiting = 0;  // 清除等待标记}pthread_mutex_unlock(&ep->cdmtx);  // 释放条件变量锁// 使用自旋锁快速保护就绪队列的读取操作pthread_spin_lock(&ep->lock);// 将就绪事件从 rdlist 复制到用户提供的 events 数组int cnt = 0;int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum);  // 取较小值避免越界int i = 0;// 按边缘触发模式(EPOLLET)处理就绪队列while (num != 0 && !LIST_EMPTY(&ep->rdlist)) {struct epitem *epi = LIST_FIRST(&ep->rdlist);LIST_REMOVE(epi, rdlink);  // 从就绪链表移除epi->rdy = 0;  // 清除就绪标记memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));num--;cnt++;ep->rdnum--;  // 减少就绪计数}pthread_spin_unlock(&ep->lock);  // 释放自旋锁return cnt;  // 返回就绪事件数量
}

这段代码实现了 epoll 机制的核心等待逻辑:

  1. 参数验证
    • 检查 epfd 有效性
    • 验证 events 数组和 maxevents 参数
  2. 等待逻辑
    • 使用条件变量 ep->cond 实现阻塞等待
    • 根据timeout参数处理三种情况:
      • timeout > 0:带超时的等待(计算绝对时间)
      • timeout < 0:永久等待
      • timeout = 0:立即返回
  3. 事件收集
    • 使用自旋锁 ep->lock 快速保护就绪队列操作
    • 将就绪事件从 rdlist 链表复制到用户数组
    • 更新就绪计数 rdnum

关键特性:

  1. 并发控制
    • 使用互斥锁 ep->cdmtx + 条件变量 ep->cond 实现等待 / 唤醒机制
    • 使用自旋锁 ep->lock 保护高性能的就绪队列操作
  2. 超时处理
    • 支持相对时间到绝对时间的转换(struct timespec
    • 处理纳秒级时间计算和溢出
  3. 边缘触发支持
    • 通过 LIST_REMOVE 从就绪队列移除事件节点
    • 清除 rdy 标记确保事件不会重复触发
  4. 错误处理
    • 处理锁获取失败(EDEADLK
    • 处理条件等待错误(pthread_cond_wait 失败)
LT/ET 的实现
  • LT(Level Triggered,水平触发)和 ET(Edge Triggered,边缘触发)是 epoll 的两种触发模式。
    • LT:只要文件描述符对应的设备有未处理的事件,epoll_wait 就会返回该事件,适合以阻塞方式处理 I/O 的应用程序。
    • ET:只有当文件描述符对应的设备状态发生变化(如从不可读变为可读)时,epoll_wait 才会返回该事件,适用于以非阻塞方式处理 I/O 且要求高效处理的场景,能减少不必要的事件通知。

「代码实现」

下一章:2.1.2 事件驱动reactor的原理与实现

相关文章:

  • 数据结构 哈希表、栈的应用与链式队列 6.29 (尾)
  • 模拟工作队列 - 华为OD机试真题(JavaScript卷)
  • Python 数据分析与可视化 Day 11 - 特征工程基础
  • 从0开始学linux韦东山教程Linux驱动入门实验班(3)
  • python中多线程:线程插队方法join详解、线程停止、通过变量来让线程停止
  • Java面试宝典:基础五
  • 【数据集】中国2016-2022年 城市土地利用数据集 CULU
  • 操作系统学习笔记 | 操作系统常见问题整理
  • AlphaFold3安装报错
  • NumPy 统计函数与矩阵运算指南
  • AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年6月29日第123弹
  • 理解 Confluent Schema Registry:Kafka 生态中的结构化数据守护者
  • 数据库级联操作详解:级联删除、更新与置空
  • aws(学习笔记第四十八课) appsync-graphql-dynamodb
  • 详解快速排序
  • STM32——HAL库总结
  • acme自签证书
  • docker安装gitlab并配置ssl证书
  • DeepSeek贪吃蛇游戏网页版
  • python打卡 DAY 46 通道注意力(SE注意力)