目录
- 手写 DPDK 协议栈(十二):TCP 并发与自实现 epoll 的就绪事件分发
- 1. 结构:注册表和就绪表不能混在一起
- 2. 从 TCP 数据到 EPOLLIN
- 3. 使用方式
- 4. 并发语义的三个难点
- 小结
手写 DPDK 协议栈(十二):TCP 并发与自实现 epoll 的就绪事件分发
标签:DPDKTCPepoll并发红黑树
tcp_multi/在 TCP socket 基础上实现nepoll_create、nepoll_ctl与nepoll_wait。它展示了 epoll 的本质:注册表记录“关心什么”,就绪队列记录“现在有什么可处理”,等待者只从就绪队列取事件。
1. 结构:注册表和就绪表不能混在一起
epoll instance ├─ 红黑树 rbr:sockfd -> epitem,支持 ctl ADD/MOD/DEL ├─ 就绪链表 rdlist:仅保存已发生事件的 epitem └─ cond + mutex:没有事件时阻塞/唤醒 nepoll_wait课程eventpoll使用红黑树查找注册项,使用链表维护 ready list,并分别对树与就绪表设置同步机制。这样不会在每次wait时扫描全部 fd。
structeventpoll{ep_rb_tree rbr;intrbcnt;LIST_HEAD(,epitem)rdlist;intrdnum;pthread_mutex_tmtx;pthread_spinlock_tlock;pthread_cond_tcond;};2. 从 TCP 数据到 EPOLLIN
TCP 协议线程收到 PSH 数据后,将tcp_offload_t放入连接的inring;socket 层再调用事件回调,将对应epitem放入 ready list 并唤醒等待线程。
NIC RX -> TCP ESTABLISHED -> socket.in ring -> nepoll_event_callback(ep, sockfd, EPOLLIN) -> rdlist -> nepoll_wait 返回 -> nrecv 读取 socket.inEPOLLOUT通常在 socket 可继续发送时产生;EPOLLERR、EPOLLHUP和EPOLLRDHUP则要在 FIN、RST、队列错误等状态转移中明确生成,不能凭空假定。
3. 使用方式
intepfd=nepoll_create(1024);structepoll_eventev={.events=EPOLLIN,.data.fd=connfd};nepoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);structepoll_eventevents[128];intn=nepoll_wait(epfd,events,128,-1);for(inti=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 数据转为应用可等待的事件。三层之间通过明确的队列和所有权衔接,才能在并发连接下既快又可维护。
学习链接: https://github.com/0voice