go/0014-网络IO
epoll
- 红黑树管理 FD:通过
epoll_ctl
将 FD 注册到内核的红黑树中。 - 就绪链表优化:当事件发生时,内核将 FD 加入就绪链表,
epoll_wait
直接从链表获取就绪 FD。 - 事件驱动机制:使用
mmap
避免用户态与内核态之间的数据复制。
# 三个函数
epoll_create
epoll_ctl
epoll_wait
fd
listen_fd
client_fd
处理 TCP 数据
epoll
通过epoll_ctl
管理两类 FD:listen fd
(监听新连接)和client fd
(监听数据)。- 事件驱动:
epoll_wait
阻塞等待,仅在事件发生时唤醒,避免轮询消耗 CPU。 - 分工明确:
listen fd
的EPOLLIN
→ 用非阻塞accept
接收新连接,注册client fd
到epoll
。client fd
的EPOLLIN
→ 用非阻塞recv
读取数据,处理后可回显或转发。
- 非阻塞 + 事件通知结合:既避免了阻塞导致的效率问题,又解决了非阻塞单独使用时的 “忙等” 问题。
epoll 处理监听的 TCP 套接字(listen fd)数据到来的过程,本质是通过事件驱动机制检测客户端连接请求,并触发应用程序处理连接的流程。
前提:监听套接字的准备
首先需要创建一个监听的 TCP 套接字(listen_fd
),并完成初始化:
- 调用
socket()
创建 TCP 套接字(SOCK_STREAM
类型)。 - 调用
bind()
将套接字绑定到指定的 IP 和端口。 - 调用
listen()
将套接字转为监听状态(此时listen_fd
开始接收客户端的连接请求)。 - (可选但推荐)将
listen_fd
设置为非阻塞模式(通过fcntl
设置O_NONBLOCK
),避免后续accept
操作阻塞程序。
步骤 1:将监听套接字注册到 epoll
要让 epoll 监控 listen_fd
的连接请求,需先将其加入 epoll 的监控列表:
-
调用
epoll_create
创建一个 epoll 实例(返回epoll_fd
)。 -
调用
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
(表示接收队列已空),避免漏接连接。
总结流程
- 初始化监听套接字(
listen_fd
)并设置为非阻塞。 - 创建 epoll 实例,将
listen_fd
加入 epoll 并监控EPOLLIN
事件(可选 ET 模式)。 - 循环调用
epoll_wait
等待事件。 - 当
listen_fd
触发EPOLLIN
事件时,循环调用accept
获取所有客户端连接(client_fd
)。 - 将
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
特性 | select | poll | epoll |
---|---|---|---|
数据结构 | 位图(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