绿盟 C++开发二面:epoll 触发模式与长连接治理
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请先做一下自我介绍
- 一个网络数据包从网卡到达用户态服务,通常会经过哪些关键环节?
- 网络服务端需要支持大量长连接时,怎样设计连接管理和资源控制?
- epoll 的 LT 和 ET 模式有什么区别?ET 模式下为什么通常要循环读写直到返回 EAGAIN?
- TCP 粘包和拆包是如何产生的?应用层协议应如何解决?
《参考解析》
数据包从网卡到用户态的路径。 大致是:网卡收到帧,通过 DMA 写入接收队列对应的内存环(rx_ring);内核网络驱动在中断或 NAPI 轮询中处理,走协议栈(链路层 → IP → TCP),最终把载荷放进该 socket 的接收缓冲区;用户态通过 recv/epoll 把数据取走再做协议解析和业务处理。说这条链路的目的不是背名词,而是把延迟拆段:网卡收包(队列/RSS 是否均衡)、内核协议栈(软中断 ksoftirqd 是否打满)、socket 缓冲区排队、线程调度、应用处理。线上排查高延迟时必须先分清是哪一段慢,否则容易在应用层瞎优化;旁路方案(DPDK/XDP)之所以快,本质就是砍掉了内核协议栈这一段。
海量长连接的资源账。 连接数不能只盯着文件描述符上限,要把每一项成本算进去:每条连接的操作系统 socket 结构、收发缓冲区、用户态连接对象、以及待发送的数据积压。设计上通常用非阻塞 socket 加 epoll 做事件驱动,把连接管理与业务处理解耦,避免一条慢连接阻塞事件循环。关键的控制点有四个:连接数上限(防止 fd 耗尽)、单连接缓冲区上限(防止慢客户端把内存吃光)、空闲超时与心跳(心跳间隔要小于中间设备的空闲断链时间)、写侧背压(发送缓冲积压到阈值时停止读入甚至断开,否则内存会持续上涨)。再往下就是 SO_REUSEPORT、多 Reactor 线程、连接迁移这些扩展手段。
LT 与 ET 的区别,以及为什么 ET 要读到 EAGAIN。 LT(水平触发)在 fd 仍然可读写时会持续通知,哪怕你一次只读了一部分,下次 epoll_wait 还会返回——编程简单但可能空转。ET(边缘触发)只在状态变化时通知一次,之后无论缓冲区里还剩多少数据都不再提醒。所以在 ET 下如果只读了一次就返回,剩下的数据就会永远躺在缓冲区里,连接看起来「卡住」——这就是必须把 socket 设成非阻塞并循环 recv 直到返回 EAGAIN/EWOULDBLOCK 的原因。循环里还要处理三种情况:n > 0 继续读、n == 0 表示对端正常关闭要断连、errno == EINTR 要重试,其余错误才关闭连接。写侧同理,要处理部分写和 EAGAIN,把剩余数据挂进应用层发送队列等可写事件。
粘包与拆包的成因和定界方案。 TCP 是字节流,只保证有序可靠,不保留应用层的消息边界:一次 send 的数据可能被拆成多个报文到达,多次 send 也可能合并后被一次 recv 读出——这不是 TCP 的缺陷,而是它的语义。因此定界必须由应用层协议负责,常见四种:固定长度(简单但不灵活)、分隔符(如按 \n,需要处理数据里出现分隔符的转义)、长度前缀(先读固定长度的头部拿长度,再读够正文,最通用)、以及头部加长度字段的二进制协议(如 TLV)。工程上还要注意:定界逻辑必须能处理半包(读到的不足一个完整消息时要缓存下来等下次)、recv 返回值可能小于请求长度、以及单条消息超过缓冲区上限时要拒绝而不是无限增长——否则会变成内存耗尽的可利用点。