目录
一、poll函数
1.函数原型
2.参数说明
3.struct pollfd 结构体
4.返回值
5.使用步骤
6.与 select 的对比
7.适用场景
8.缺点
9.总结
二、epoll函数
1.核心思想
2.核心函数
1. epoll_create - 创建 epoll 实例
2. epoll_ctl - 管理 epoll 事件表
3. epoll_wait - 等待事件发生
3.事件类型
4.触发模式
1. 水平触发(LT,默认)
2. 边缘触发(ET)
5.使用步骤
6.示例代码(LT 模式)
7.优点
8.缺点
9.适用场景
10.对比 select/poll
11.注意事项
12.总结
三、当网卡接收到数据流程
1. 网卡接收数据(硬件层)
2. epoll 的事件触发(内核层)
关键数据结构
事件触发流程
3. 用户层获取事件(应用层)
流程总结(结合数据结构)
性能优势的根源
触发模式的影响
1. 水平触发(LT)
2. 边缘触发(ET)
完整流程示例
总结
一、poll函数
1.函数原型
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.参数说明
-
fds
指向struct pollfd
结构体数组的指针,每个结构体描述一个要监视的文件描述符及其关注的事件。 -
nfds
数组fds
的长度(即监视的文件描述符数量)。 -
timeout
超时时间(毫秒):-
-1
: 阻塞等待,直到某个文件描述符就绪。 -
0
: 立即返回,不阻塞。 -
>0
: 最多等待指定毫秒数。
-
3.struct pollfd
结构体
struct pollfd {int fd; // 监视的文件描述符short events; // 关注的事件(输入)short revents; // 实际发生的事件(输出)
};
-
events
:应用程序关注的事件(按位或组合): -
-
POLLIN
:数据可读。 -
POLLOUT
:数据可写。 -
POLLERR
:发生错误。 -
POLLHUP
:连接挂起(如对端关闭)。 -
POLLNVAL
:文件描述符未打开。
-
-
revents
:内核返回的实际事件,可能包含events
中的事件或错误标志(如POLLERR
)。
4.返回值
-
>0
:就绪的文件描述符数量。 -
0
:超时且没有文件描述符就绪。 -
-1
:发生错误,可通过errno
获取原因(如EINTR
表示被信号中断)。
5.使用步骤
-
初始化
struct pollfd
数组,设置要监视的fd
和events
。 -
调用
poll
,阻塞等待事件发生。 -
检查返回的
revents
,处理就绪的文件描述符。
示例代码:
struct pollfd fds[2];
fds[0].fd = sock_fd; // 监视套接字
fds[0].events = POLLIN; // 关注可读事件
fds[1].fd = pipe_fd; // 监视管道
fds[1].events = POLLOUT; // 关注可写事件int ret = poll(fds, 2, 1000); // 等待1秒
if (ret > 0) {if (fds[0].revents & POLLIN) {// 套接字可读,处理数据}if (fds[1].revents & POLLOUT) {// 管道可写,发送数据}
}
6.与 select
的对比
-
文件描述符数量限制:
-
select
使用固定大小的fd_set
,受FD_SETSIZE
限制(通常 1024)。 -
poll
使用动态数组,理论上无限制。
-
-
效率:
-
select
每次调用需重新传入所有文件描述符。 -
poll
通过分离events
和revents
字段,避免重复初始化。
-
-
可移植性:
-
select
在所有系统可用。 -
poll
在部分旧系统(如早期 Windows)不支持。
-
7.适用场景
-
需要监视的文件描述符数量较多(超过
FD_SETSIZE
)。 -
希望避免
select
的重复初始化开销。 -
对跨平台兼容性要求不高。
8.缺点
-
当监视大量文件描述符时,遍历所有
struct pollfd
检查revents
效率较低(此时epoll
或kqueue
更高效)。
9.总结
poll
解决了 select
的部分缺陷,适合中等规模的高效 I/O 多路复用。在 Linux 上处理海量连接时,更推荐使用 epoll
;而在 macOS/BSD 上,kqueue
是更优的选择。
二、epoll函数
1.核心思想
-
事件驱动:仅关注活跃的文件描述符,避免遍历全部描述符。
-
内核事件表:通过内核维护的红黑树和就绪链表,实现高效的事件注册与通知。
2.核心函数
1. epoll_create
- 创建 epoll 实例
#include <sys/epoll.h>int epoll_create(int size); // size 参数已弃用(只需 >0)
-
返回一个
epoll
文件描述符(用于后续操作)。 -
不再使用时需用
close()
关闭。
2. epoll_ctl
- 管理 epoll 事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
参数:
-
epfd
:epoll_create
返回的 epoll 描述符。 -
op
:操作类型:-
EPOLL_CTL_ADD
:注册新 fd。 -
EPOLL_CTL_MOD
:修改已注册的 fd。 -
EPOLL_CTL_DEL
:删除 fd。
-
-
fd
:要操作的 fd(如套接字)。 -
event
:指向事件结构体的指针。
-
-
struct epoll_event
:struct epoll_event {uint32_t events; // 关注的事件(按位或组合)epoll_data_t data; // 用户数据(如关联的 fd) };typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64; } epoll_data_t;
3. epoll_wait
- 等待事件发生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
参数:
-
epfd
:epoll 描述符。 -
events
:输出参数,用于接收就绪事件数组。 -
maxevents
:events
数组的最大容量。 -
timeout
:超时时间(毫秒),-1
表示阻塞,0
表示立即返回。
-
-
返回值:
-
成功:返回就绪的 fd 数量。
-
超时:返回
0
。 -
错误:返回
-1
,设置errno
。
-
3.事件类型
事件类型 | 说明 |
---|---|
EPOLLIN | 数据可读(包括对端关闭连接) |
EPOLLOUT | 数据可写 |
EPOLLERR | 发生错误(自动监听,无需显式设置) |
EPOLLHUP | 对端关闭连接或读写关闭 |
EPOLLET | 边缘触发模式(默认是水平触发) |
EPOLLONESHOT | 事件只触发一次,需重新注册才能继续监听 |
4.触发模式
1. 水平触发(LT,默认)
-
特点:只要 fd 满足就绪条件,会持续通知。
-
行为:类似
select
/poll
,未处理的事件会重复触发epoll_wait
。 -
适用场景:简单编程模型,无需一次性处理完所有数据。
2. 边缘触发(ET)
-
特点:仅在 fd 状态变化时通知一次。
-
行为:需一次性处理完所有数据(否则可能丢失事件)。
-
要求:必须使用非阻塞 I/O,循环读/写直到
EAGAIN
或EWOULDBLOCK
。 -
适用场景:高性能场景,减少事件触发次数。
5.使用步骤
-
调用
epoll_create
创建 epoll 实例。 -
调用
epoll_ctl
注册需要监听的 fd 及事件。 -
循环调用
epoll_wait
等待事件。 -
处理就绪事件(如读/写数据)。
-
根据需求修改或删除已注册的 fd。
6.示例代码(LT 模式)
#include <sys/epoll.h>#define MAX_EVENTS 10int main() {int epfd = epoll_create1(0); // 创建 epoll 实例struct epoll_event ev, events[MAX_EVENTS];// 注册 socket_fd 到 epoll,监听可读事件(默认 LT)ev.events = EPOLLIN;ev.data.fd = socket_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &ev);while (1) {int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);for (int i = 0; i < nready; i++) {if (events[i].data.fd == socket_fd) {// 处理 socket 可读事件(如 accept 新连接)} else {// 处理其他 fd 的读/写事件}}}close(epfd);return 0;
}
7.优点
-
高效处理海量连接:时间复杂度
O(1)
(就绪事件数量相关)。 -
无需重复注册:内核直接维护事件表。
-
支持边缘触发:减少无效事件通知,提高吞吐量。
8.缺点
-
仅限 Linux:跨平台需改用
select
/poll
或kqueue
(BSD/macOS)。 -
编程复杂度:ET 模式需处理非阻塞 I/O 和缓冲区。
9.适用场景
-
高并发服务器(如 Web 服务器、游戏服务器)。
-
需要同时处理数万甚至百万级连接。
-
追求低延迟和高吞吐量的网络应用。
10.对比 select
/poll
特性 | select /poll | epoll |
---|---|---|
时间复杂度 | O(n)(遍历所有 fd) | O(1)(仅处理就绪事件) |
最大 fd 数量 | 有限(select 默认 1024) | 理论无上限(受系统资源限制) |
触发模式 | 仅水平触发(LT) | 支持 LT 和 ET |
内存拷贝 | 每次调用需拷贝 fd 集合到内核 | 内核直接管理事件表 |
适用场景 | 低并发或跨平台 | Linux 高并发 |
11.注意事项
-
ET 模式必须使用非阻塞 I/O:避免因未读完数据导致线程阻塞。
-
避免
EPOLLONESHOT
误用:需手动重新注册事件。 -
内存泄漏:及时关闭不再使用的 epoll 描述符。
12.总结
epoll
是 Linux 下高性能网络编程的基石,尤其适合高并发场景。其事件驱动模型和高效的内核机制(如红黑树、就绪链表)使其在处理海量连接时性能远超 select
和 poll
。若需跨平台,可结合 epoll
(Linux)、kqueue
(BSD/macOS)和 IOCP
(Windows)实现多路复用。
三、当网卡接收到数据流程
1. 网卡接收数据(硬件层)
-
数据到达网卡:
-
网卡接收到数据包后,通过 DMA 直接将数据拷贝到内核内存的 环形缓冲区(Ring Buffer) 中。
-
网卡触发 硬件中断,通知 CPU 有数据到达。
-
-
软中断处理:
-
内核通过 ksoftirqd 线程(软中断)处理数据包:
-
解析数据包:拆解以太网帧、IP 头、TCP/UDP 头。
-
关联 Socket:根据端口和 IP 找到对应的 Socket(文件描述符
fd
)。 -
填充接收缓冲区:将数据存入该 Socket 的 接收缓冲区(Receive Buffer)。
-
-
2. epoll 的事件触发(内核层)
关键数据结构
-
红黑树(RB-Tree):存储所有通过
epoll_ctl
注册的fd
(键为fd
,值为epoll_event
)。 -
就绪队列(Ready List):存放已就绪的
fd
对应的事件(epoll_item
结构)。
事件触发流程
-
检查 epoll 监听状态:
-
当数据被存入 Socket 的接收缓冲区后,内核检查该
fd
是否被某个epoll
实例监听(即是否在红黑树中存在对应的epoll_item
)。 -
如果是,内核会将该
fd
关联的epoll_item
添加到 就绪队列 中,并标记事件类型(如EPOLLIN
)。
-
-
回调机制:
-
epoll
通过 回调函数(Callback) 实现事件触发,而非轮询。 -
当数据到达时,内核直接调用
ep_poll_callback
函数,将对应的epoll_item
插入就绪队列,无需遍历红黑树。
-
3. 用户层获取事件(应用层)
-
调用
epoll_wait
:-
用户程序调用
epoll_wait
时,内核检查 就绪队列:-
如果队列非空,直接返回就绪的
fd
列表(通过events
数组)。 -
如果队列为空,根据
timeout
参数决定阻塞或超时返回。
-
-
-
就绪队列处理:
-
内核将就绪队列中的
epoll_item
复制到用户提供的events
数组中,并清空就绪队列。 -
用户程序遍历
events
数组,处理每个就绪的fd
(如读取数据)。
-
流程总结(结合数据结构)
-
数据到达网卡 → DMA 到内核缓冲区 → 协议栈解析 → 填充 Socket 接收缓冲区。
-
检查红黑树 → 若
fd
被epoll
监听 → 触发回调函数 → 将epoll_item
加入就绪队列。 -
用户调用
epoll_wait
→ 内核返回就绪队列中的事件 → 应用处理数据。
性能优势的根源
-
红黑树的高效管理:
-
添加/删除/查找
fd
的时间复杂度为O(log N)
,适合动态管理海量连接。
-
-
就绪队列的 O(1) 事件获取:
-
直接返回就绪的
fd
,无需遍历所有监听的fd
(对比select/poll
的O(N)
)。
-
-
回调驱动(而非轮询):
-
仅在实际事件发生时触发操作,避免无意义的 CPU 开销。
-
触发模式的影响
1. 水平触发(LT)
-
数据未读完时:内核会持续将
fd
加入就绪队列,直到接收缓冲区为空。 -
行为示例:
若应用只读取部分数据,下次epoll_wait
仍会返回该fd
的EPOLLIN
事件。
2. 边缘触发(ET)
-
仅在状态变化时触发:内核仅在接收缓冲区从空变为非空时,将
fd
加入就绪队列一次。 -
行为示例:
应用必须一次性读取所有数据(直到read
返回EAGAIN
),否则会丢失后续事件。
完整流程示例
-
客户端发送数据 → 网卡接收 → 内核协议栈处理 → 数据存入 Socket 接收缓冲区。
-
内核检查红黑树:发现该
fd
被epoll
监听(关注EPOLLIN
事件)。 -
触发回调 → 将该
fd
的epoll_item
插入就绪队列。 -
用户调用
epoll_wait
→ 获取到该fd
的EPOLLIN
事件。 -
应用读取数据 → 若使用 ET 模式,需循环读取直到
EAGAIN
。 -
处理完毕 → 继续调用
epoll_wait
等待新事件。
总结
-
红黑树:管理所有注册的
fd
,实现高效增删改查。 -
就绪队列:存储实际发生的事件,避免全量遍历。
-
回调机制:数据到达时直接触发事件,减少延迟。