poll
epoll 是 Linux 专属的 IO 多路复用 ,用来代替 select / poll,高并发场景下性能远优于 select,是 Linux 高并发网络编程核心。
| 接口 | 时间复杂度 | 最大 fd 上限 | 内核实现 | 触发方式 |
|---|---|---|---|---|
| select | O(n) | 1024(默认) | 轮询扫描所有 fd | 水平触发 LT |
| poll | O(n) | 无硬性上限 | 轮询扫描所有 fd | 水平触发 LT |
| epoll | O (1) 就绪回调 | 无硬性上限 | 回调 + 就绪链表 | LT / ET 二选一 |
1. epoll_create
-
成功:返回一个 epoll 实例的文件描述符
epfd -
失败:
‑1,设置 errno#include <sys/epoll.h>
int epoll_create(int size);
Linux 2.6.8 之后,size 不再用来限定 epoll 能监听的 fd 最大数量
但是size 必须传一个大于 0 的整数,比如 1
2. epoll_ctl
向内核 epoll 实例添加、修改、删除要监听的文件描述符。epoll 唯一的管理接口。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op 参数三个选项:
EPOLL_CTL_ADD:添加 fd 到 epoll 监听EPOLL_CTL_MOD:修改 fd 监听事件EPOLL_CTL_DEL:从 epoll 移除 fd
struct epoll_event
一般定义为结构体数组,数组的长度一般定义为,一次调用epoll_wait可以响应多少个就绪事件
- epoll 里面一共监听了 5000 个客户端 fd(红黑树 5000 个节点)
- 你定义:
struct epoll_event events[256];,maxevents=256
含义:
某一瞬间最多一次返回 256 个就绪事件。 如果同一时刻有300 个 fd 同时就绪 : epoll_wait 先返回前 256 个;剩下 44 个就绪的还留在内核就绪链表,下一次调用 epoll_wait 再拿剩下的。
struct epoll_event {
uint32_t events; // 监听事件
epoll_data_t data; // 用户数据,一般存fd
};
events 常用宏:
EPOLLIN:可读事件EPOLLOUT:可写事件EPOLLERR:出错,自动监听,不用手动加EPOLLHUP:挂起断开EPOLLET:边缘触发 ET
3. epoll_wait
-
epfd epoll 实例的文件描述符,来自
epoll_create1()。 -
events(传出参数) 结构体数组首地址,内核会把已经就绪的事件拷贝到这个数组里。
-
maxevents events 数组最多能存放多少个事件,不能填 0。
例:
struct epoll_event evs[1024];→ maxevents = 1024
- timeout 超时时间,单位:毫秒
-1:永久阻塞,直到有事件到来0:非阻塞,立刻返回,没有就绪事件就返回 0>0:等待指定毫秒,超时后返回 0运行代码
返回值
-
> 0:就绪 fd 的数量;返回多少,就遍历 events 0 \~ ret‑1 -
0:超时,没有任何事件就绪 -
-1:出错,perror查看原因#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
练习:使用epoll实现TCP服务器非阻塞接收后回传
/**
* @brief 将指定文件描述符添加到epoll的监听列表
* @param epfd epoll实例的文件描述符
* @param fd 需要被监听的文件描述符
* @return 成功返回0,失败返回‑1
*/
int epoll_add(int epfd,int fd)
{
// 定义epoll事件结构体,存放待监听的事件信息
struct epoll_event ev;
// 设置监听事件类型:EPOLLIN → 可读事件
ev.events = EPOLLIN;
// 绑定要监听的文件描述符,事件就绪后可通过data.fd取出
ev.data.fd = fd;
// epoll_ctl:对epoll实例执行控制操作
// EPOLL_CTL_ADD:添加fd到epoll监听树上
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) < 0)
{
perror("epoll ctl fail");
return -1;
}
return 0;
}
// 创建epoll实例,参数2为历史遗留参数,大于0即可
int epfd = epoll_create(2);
// 判断epoll创建是否失败
if(epfd < 0)
{
perror("epoll_create fail");
return -1;
}
// 将标准输入fd=0添加到epoll监听树上
epoll_add(epfd,0);
// 将通信套接字connfd添加到epoll监听树上
epoll_add(epfd,connfd);
// 获取标准输入原来的文件状态标志
int flag = fcntl(0,F_GETFL);
// 设置标准输入为非阻塞模式
fcntl(0,F_SETFL,flag | O_NONBLOCK);
// 获取通信套接字原来的文件状态标志
flag = fcntl(connfd,F_GETFL);
// 设置connfd套接字为非阻塞模式
fcntl(connfd,F_SETFL,flag | O_NONBLOCK);
// 事件循环,持续等待epoll事件
while(1)
{
// 存放就绪事件的数组,最多2个事件
struct epoll_event rev[2];
// 阻塞等待就绪事件,-1代表无限阻塞
int ret = epoll_wait(epfd,rev,2,-1);
// epoll_wait出错判断
if(ret < 0)
{
perror("epoll_wait fail");
return -1;
}
// 打印本次就绪的文件描述符数量
printf("ret = %d\n",ret);
// 接收数据缓冲区,初始清零
char buf[1024] = {'\0'};
int i = 0;
// 遍历本次所有就绪的文件描述符
for(i = 0;i<ret;i++)
{
// 如果就绪的是标准输入(键盘)
if(rev[i].data.fd == 0)
{
// 从键盘读取一行输入
fgets(buf,sizeof(buf),stdin);
printf("buf = %s\n",buf);
}
// 如果就绪的是网络通信套接字
else if(rev[i].data.fd == connfd)
{
// 从套接字读取客户端发来的数据
int ret = read(connfd,buf,sizeof(buf));
// 判断收到quit指令,退出程序
if(strncmp(buf,"quit",4) == 0)
{
close(connfd);
exit(0);
}
// 打印读到的字节数和接收的数据
printf("ret = %d buf = %s \n",ret,buf);
// 组装回显应答字符串
char sbuf[1024];
sprintf(sbuf,"server+%s\n",buf);
// 将处理后的数据写回客户端
write(connfd,sbuf,strlen(sbuf)+1);
}
}
}
LT 水平触发 & ET 边缘触发
- LT (水平触发):只要数据还在缓冲区,就一直通知你
- ET (边缘触发):状态一变,只通知一次;剩下的数据不再提醒
客户端给服务器发 2KB 数据,服务器一次只 read 了 1KB,缓冲区还剩 1KB。
LT 水平触发(默认,不加 EPOLLET)
- epoll_wait 返回可读事件
- read 读到 1KB,缓冲区还剩 1KB
- 下一轮 epoll_wait 立刻又会通知可读
只要缓冲区有数据,事件就一直 "处于就绪状态",反复唤醒你。
ET 边缘触发(加 EPOLLET)
- epoll_wait 返回可读事件
- read 读到 1KB,缓冲区还剩 1KB
- 不会再通知你了!剩下 1KB 永远留在缓冲区等下一次新数据到来才会再触发
只有从无数据 → 有数据这个跳变瞬间通知一次。
| 对比项 | LT 水平触发 Level‑Triggered | ET 边缘触发 Edge‑Triggered | |
|---|---|---|---|
| 触发条件 | 缓冲区有未读数据 | 状态发生变化 (无→有) | |
| 通知次数 | 只要有剩余数据,每次 epoll_wait 都通知 | 变化瞬间仅通知一次 | |
| fd 阻塞要求 | 阻塞 / 非阻塞都可以 | 必须非阻塞 O_NONBLOCK | |
| 读数据策略 | 一次读一点,剩下下次再处理 | 收到事件后循环读直到 read 返回 EAGAIN | |
| 性能开销 | 唤醒次数多 | 唤醒次数少,效率更高 | |
| 难度 | 简单,不容易丢数据 | 容易踩坑,难度高 | |
| 默认模式 | epoll 默认开启 | 需要手动 ` | EPOLLET |
使用边缘触发,必须手动设置,添加fd监听的时候,设置为边缘触发模式EPOLLET,并且将文件描述符设置为非阻塞状态O_NONBLOCK
int epoll_add(int epfd,int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) < 0)
{
perror("epoll ctl fail");
return -1;
}
return 0;
}
int set_nonblock(int fd)
{
int flag = fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag | O_NONBLOCK);
return 0;
}
什么时候选 LT,什么时候选 ET
LT(水平触发)适用场景
- 初学 epoll,快速开发
- 简单服务器,单线程,不想写循环读写
- 大部分工程里的简单网络程序
ET(边缘触发)适用场景
- 高并发、上万连接,想要减少 epoll 唤醒次数
- 高性能 Reactor 服务器,如 libevent、nginx (可选 ET)