Skip to main content

go/0014-网络IO

· 8 min read

epoll

  • 红黑树管理 FD:通过 epoll_ctl 将 FD 注册到内核的红黑树中。
  • 就绪链表优化:当事件发生时,内核将 FD 加入就绪链表,epoll_wait 直接从链表获取就绪 FD。
  • 事件驱动机制:使用 mmap 避免用户态与内核态之间的数据复制。
# 三个函数
epoll_create
epoll_ctl
epoll_wait

fd
listen_fd
client_fd

处理 TCP 数据

  1. epoll 通过 epoll_ctl 管理两类 FD:listen fd(监听新连接)和 client fd(监听数据)。
  2. 事件驱动:epoll_wait 阻塞等待,仅在事件发生时唤醒,避免轮询消耗 CPU。
  3. 分工明确:
    • listen fd 的 EPOLLIN → 用非阻塞 accept 接收新连接,注册 client fd 到 epoll
    • client fd 的 EPOLLIN → 用非阻塞 recv 读取数据,处理后可回显或转发。
  4. 非阻塞 + 事件通知结合:既避免了阻塞导致的效率问题,又解决了非阻塞单独使用时的 “忙等” 问题。

epoll 处理监听的 TCP 套接字(listen fd)数据到来的过程,本质是通过事件驱动机制检测客户端连接请求,并触发应用程序处理连接的流程。

前提:监听套接字的准备

首先需要创建一个监听的 TCP 套接字(listen_fd),并完成初始化:

  1. 调用 socket() 创建 TCP 套接字(SOCK_STREAM 类型)。
  2. 调用 bind() 将套接字绑定到指定的 IP 和端口。
  3. 调用 listen() 将套接字转为监听状态(此时 listen_fd 开始接收客户端的连接请求)。
  4. (可选但推荐)将 listen_fd 设置为非阻塞模式(通过 fcntl 设置 O_NONBLOCK),避免后续 accept 操作阻塞程序。

步骤 1:将监听套接字注册到 epoll

要让 epoll 监控 listen_fd 的连接请求,需先将其加入 epoll 的监控列表:

  1. 调用 epoll_create 创建一个 epoll 实例(返回 epoll_fd)。

  2. 调用 epoll_ctl 向 epoll 实例添加 listen_fd,并指定要监控的事件:

    • 核心关注事件:EPOLLIN(表示 “可读”)。因为当客户端发起连接请求(三次握手完成后),内核会将连接请求放入 listen_fd 的接收队列,此时 listen_fd 处于 “可读” 状态。
    • 可选设置:EPOLLET(边缘触发模式),相比默认的水平触发模式(LT),ET 模式更高效(仅在事件状态变化时通知一次)。
int epoll_fd = epoll_create1(0); // 创建epoll实例 
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 监控"可读"事件,边缘触发
ev.data.fd = listen_fd; // 关联监听fd
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev); // 添加到epoll

步骤 2:epoll 等待连接请求事件

应用程序通过 epoll_wait 阻塞等待事件(可设置超时时间):

struct epoll_event events[1024];  // 存储就绪事件的数组
int nfds = epoll_wait(epoll_fd, events, 1024, -1); // 阻塞等待事件
  • 当没有客户端连接请求时,epoll_wait 会阻塞,不占用 CPU 资源。
  • 当有客户端发起连接(三次握手完成),内核会标记 listen_fd 为 “可读”,并将其加入 epoll 的就绪队列,此时 epoll_wait 会返回,nfds 为就绪事件的数量。

步骤 3:处理监听套接字的 “可读” 事件

epoll_wait 返回后,遍历就绪事件数组,判断是否是监听套接字的事件:

for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) { // 监听fd有事件
// 处理连接请求
}
}

此时 “可读” 事件的含义是:listen_fd 的接收队列中存在未处理的客户端连接请求,需要调用 accept 获取新连接。

步骤 4:通过 accept 获取客户端连接

当确认是监听套接字的 “可读” 事件后,需调用 accept 从 listen_fd 的接收队列中取出客户端连接:

  • accept 会返回一个新的套接字(client_fd),用于与客户端通信。
  • 由于 listen_fd 已设为非阻塞,且可能有多个连接请求(如高并发场景),需要循环调用 accept,直到返回 EAGAIN 或 EWOULDBLOCK(表示接收队列已空),避免漏接连接。

总结流程

  1. 初始化监听套接字(listen_fd)并设置为非阻塞。
  2. 创建 epoll 实例,将 listen_fd 加入 epoll 并监控 EPOLLIN 事件(可选 ET 模式)。
  3. 循环调用 epoll_wait 等待事件。
  4. 当 listen_fd 触发 EPOLLIN 事件时,循环调用 accept 获取所有客户端连接(client_fd)。
  5. 将 client_fd 加入 epoll 监控(后续处理数据读写),完成连接建立。

epoll事件有哪些?

1. EPOLLIN

可读事件,表示文件描述符可读。触发场景包括:

  • 普通数据或优先数据(如 TCP 常规数据)到达;
  • 对方发送 FIN 包(连接关闭请求),此时 read() 会返回 0;
  • 监听 socket(listen() 后的 socket)收到新的连接请求(此时可调用 accept() 接收连接)。

2. EPOLLOUT

可写事件,表示文件描述符可写。触发场景包括:

  • 内核发送缓冲区有空闲空间(可写入数据,不会阻塞);
  • 非阻塞连接(connect())完成时(无论成功或失败,都会先触发 EPOLLOUT,需通过 getsockopt() 检查连接结果)。

注意:若持续注册 EPOLLOUT,可能会频繁触发(因为发送缓冲区通常有空间),建议仅在发送数据阻塞后注册,等待可写后再发送。

3. EPOLLERR

错误事件,表示文件描述符发生错误。

  • 无需显式注册,当 fd 发生错误时(如连接失败、socket 异常),epoll 会自动触发该事件;
  • 常见场景:非阻塞连接失败、socket 被关闭后操作等。

4. EPOLLHUP

挂起事件,表示文件描述符的连接被 “挂断”。

  • 触发场景:管道的写端关闭后,读端会收到 EPOLLHUP;socket 连接被对端强制关闭(如对端进程退出)等;
  • 通常与 EPOLLIN 同时触发(如对端关闭连接时,既有 FIN 包触发 EPOLLIN,也可能伴随 EPOLLHUP)。

5. EPOLLRDHUP

TCP 对端关闭 / 半关闭事件(Linux 2.6.17 后新增),专门用于 TCP 连接。

  • 触发场景:TCP 对端调用 close() 关闭连接(发送 FIN 包),或对端关闭写半连接(shutdown(SHUT_WR));
  • 相比 EPOLLIN(可能因普通数据触发),EPOLLRDHUP 更明确地标识 “对端关闭”,避免误判。

6. EPOLLPRI

紧急数据可读事件,表示有 “优先数据”(如 TCP 带外数据 OOB)到达。

  • 触发场景:对方发送 TCP 带外数据(通过 sendmsg() 或 send() 带 MSG_OOB 标志),此时 read() 可读取带外数据。

TCP 事件如何处理

1、注册 TCP 连接到 epoll

应用程序在建立 TCP 连接后(通过 accept() 获得新的 socket fd),需要通过 epoll_ctl 将该 fd 注册到 epoll 实例中,并指定监听 EPOLLIN 事件(表示关注 “数据到来”)。

注册时,通常会将该 fd 与对应的连接上下文(如连接的结构体指针、用户信息等)关联起来,存储在 epoll_event 结构体的 data 字段中(例如 data.ptr = 连接结构体指针)。

2、epoll 等待事件就绪

应用程序通过 epoll_wait 阻塞等待事件(或非阻塞轮询)。当某个 TCP 连接有常规数据到来时,内核会标记该 fd 为 “就绪”,并将其加入 epoll 的就绪列表。

epoll_wait 会返回就绪的事件列表(数组),每个元素是 epoll_event 结构体,包含两个关键信息:

  • events:事件类型(此处为 EPOLLIN,表示有常规数据);
  • data:注册时关联的连接上下文(如连接结构体指针)。

3. 应用程序识别并处理对应连接

应用程序遍历 epoll_wait 返回的就绪事件列表,通过每个事件的 data 字段(或 data.fd)找到对应的 TCP 连接,然后调用 read() 或 recv() 读取数据。

select vs poll vs epoll

特性selectpollepoll
数据结构位图(bitmap)数组(struct pollfd红黑树 + 就绪链表
FD 数量限制通常限制为 1024(FD_SETSIZE无限制(取决于系统资源)无限制(仅受内存限制)
事件传递方式每次调用需重新设置 FD 集合每次调用需重新设置 pollfd 数组仅需通过 epoll_ctl 注册一次
就绪事件获取返回后需遍历所有 FD 检查状态返回后需遍历所有 pollfd 检查状态直接通过 epoll_wait 获取就绪 FD
触发模式仅支持水平触发(LT)仅支持水平触发(LT)支持水平触发(LT)和边缘触发(ET)
系统调用开销O(n)O(n)O(1)
应用场景少量 FD 的场景中等数量 FD 的场景大量 FD 且活跃度低的场景

网络 IO多路复用,Linux 是 epoll ,Mac 是 kqueue