目录
- [手写 DPDK 协议栈(十二):TCP 并发与自实现 epoll 的就绪事件分发](#手写 DPDK 协议栈(十二):TCP 并发与自实现 epoll 的就绪事件分发)
-
- [1. 结构:注册表和就绪表不能混在一起](#1. 结构:注册表和就绪表不能混在一起)
- [2. 从 TCP 数据到 EPOLLIN](#2. 从 TCP 数据到 EPOLLIN)
- [3. 使用方式](#3. 使用方式)
- [4. 并发语义的三个难点](#4. 并发语义的三个难点)
- 小结
手写 DPDK 协议栈(十二):TCP 并发与自实现 epoll 的就绪事件分发
标签:DPDK TCP epoll 并发 红黑树
tcp_multi/ 在 TCP socket 基础上实现 nepoll_create、nepoll_ctl 与 nepoll_wait。它展示了 epoll 的本质:注册表记录"关心什么",就绪队列记录"现在有什么可处理",等待者只从就绪队列取事件。
1. 结构:注册表和就绪表不能混在一起
text
epoll instance
├─ 红黑树 rbr:sockfd -> epitem,支持 ctl ADD/MOD/DEL
├─ 就绪链表 rdlist:仅保存已发生事件的 epitem
└─ cond + mutex:没有事件时阻塞/唤醒 nepoll_wait
课程 eventpoll 使用红黑树查找注册项,使用链表维护 ready list,并分别对树与就绪表设置同步机制。这样不会在每次 wait 时扫描全部 fd。
c
struct eventpoll {
ep_rb_tree rbr;
int rbcnt;
LIST_HEAD(, epitem) rdlist;
int rdnum;
pthread_mutex_t mtx;
pthread_spinlock_t lock;
pthread_cond_t cond;
};
2. 从 TCP 数据到 EPOLLIN
TCP 协议线程收到 PSH 数据后,将 tcp_offload_t 放入连接的 in ring;socket 层再调用事件回调,将对应 epitem 放入 ready list 并唤醒等待线程。
text
NIC RX -> TCP ESTABLISHED
-> socket.in ring
-> nepoll_event_callback(ep, sockfd, EPOLLIN)
-> rdlist
-> nepoll_wait 返回
-> nrecv 读取 socket.in
EPOLLOUT 通常在 socket 可继续发送时产生;EPOLLERR、EPOLLHUP 和 EPOLLRDHUP 则要在 FIN、RST、队列错误等状态转移中明确生成,不能凭空假定。
3. 使用方式
c
int epfd = nepoll_create(1024);
struct epoll_event ev = {.events = EPOLLIN, .data.fd = connfd};
nepoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
struct epoll_event events[128];
int n = nepoll_wait(epfd, events, 128, -1);
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN)
nrecv(events[i].data.fd, buf, sizeof(buf), 0);
}
教学接口前缀为 n,用于与 Linux 系统调用区分。应用层永远应在读取前检查返回值,处理短读、关闭与错误。
4. 并发语义的三个难点
- 只入 ready list 一次 :同一个 fd 已就绪时再次到包,不能无限重复插入;
epitem.rdy是这一去重状态。 - 事件和数据的顺序 :先把数据安全入 socket ring,再发布 EPOLLIN;否则
wait返回后可能读到空队列。 - 删除与回收 :
EPOLL_CTL_DEL、socket close、协议线程回调可能并发,必须先从注册树和 ready list 摘除,再释放对象。

小结
DPDK 负责高性能报文收发,TCP 状态机把包转为 socket 数据,epoll 再把 socket 数据转为应用可等待的事件。三层之间通过明确的队列和所有权衔接,才能在并发连接下既快又可维护。