- `epoll` 是干什么的?
- 举个简单的例子
- epoll的相关系统调用
- **epoll_create**和epoll_create1
- 区别
- epoll_ctl
- 参数解释
- **epoll_wait**
- 参数说明
- 返回值
- epoll的使用
- **epoll**工作原理
- epoll的优点(和 **select** 的缺点对应)
- epoll工作方式
- **水平触发**Level Triggered 工作模式
- 边缘触发Edge Triggered工作模式
- **对比**LT和ET
epoll
是 Linux 内核提供的一种高效的 I/O 事件通知机制,常用于网络编程中以替代传统的
select
和
poll
系统调用。相比于
select
和
poll
,
epoll
在处理大量并发连接时具有更高的性能和更好的扩展性。
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前poll的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法
epoll
是干什么的?
- 监控多个连接:
epoll
能帮你同时监控很多网络连接,就像一个超级接线员能同时管理很多电话线路一样。它能告诉你哪些连接上有新消息需要处理,哪些连接被挂断了,等等。- 高效通知:传统的
select
和poll
像是老式的接线员,每次都要检查所有的电话线路才能告诉你哪些线路有事。而epoll
就聪明多了,它只会告诉你那些有变化的线路,大大提高了效率。- 处理大量连接:如果你有上千个网络连接,
epoll
能轻松应对。它的效率不会随着连接数量的增加而明显降低,因为它只关注那些真正有事情发生的连接。
举个简单的例子
假设你在经营一个餐厅,你需要管理很多外卖订单,epoll
就像是一个超级助理,帮你监控所有的订单系统,让你知道什么时候有新订单,什么时候订单完成,什么时候客户取消订单。它只会告诉你有变化的订单,而不是每次都汇报所有的订单状态,这样你就可以专注于处理重要的事情,而不用被不必要的信息干扰。
epoll的相关系统调用
epoll 有3个相关的系统调用.
epoll_create和epoll_create1
int epoll_create(int size);
创建一个epoll的句柄.
自从linux2.6.8之后,size参数是被忽略的.
用完之后, 必须调用close()关闭.
int epoll_create1(int flags);
- flags:可以是以下值之一:
0
:不设置任何标志。EPOLL_CLOEXEC
:在返回的文件描述符上设置FD_CLOEXEC
标志,这意味着当调用exec
函数时,这个文件描述符会自动关闭。
返回值:成功时返回一个 epoll 实例的文件描述符,失败时返回 -1 并设置 errno。
区别
- 参数含义:
epoll_create
需要一个整数参数size
,但这个参数在现代 Linux 内核中已经被忽略了。epoll_create1
需要一个标志参数flags
,可以设置为0
或EPOLL_CLOEXEC
。
- 功能:
epoll_create
是老版本的接口,参数size
已经没有实际意义。epoll_create1
是新版本的接口,引入了flags
参数,增加了对EPOLL_CLOEXEC
标志的支持,使文件描述符更易于管理。
一般建议使用 epoll_create1
,因为它是更现代的接口,并且提供了更好的功能和灵活性。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数解释
epfd:由
epoll_create
或epoll_create1
创建的 epoll 实例的文件描述符。op:指定操作类型,可以是以下三个值之一:
EPOLL_CTL_ADD
:注册新的文件描述符到 epoll 实例中。EPOLL_CTL_MOD
:修改已经注册的文件描述符的监听事件。EPOLL_CTL_DEL
:从 epoll 实例中删除文件描述符。fd:需要管理的目标文件描述符。
event:指向一个
epoll_event
结构体的指针,包含需要监听的事件类型及用户数据。
epoll_event
结构体
struct epoll_event {
uint32_t events; // 监听的事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events可以是以下几个宏的集合:
EPOLLIN:表示对应的文件描述符可以进行读取操作,或者对端正常关闭了(例如,对于一个 socket,这意味着有数据可以读取,或者对端关闭了连接)。
EPOLLOUT:表示对应的文件描述符可以进行写操作(例如,对于一个 socket,这意味着可以写入数据)。
EPOLLPRI:表示对应的文件描述符有紧急的数据可读,这通常指带外数据(Out-Of-Band Data)。带外数据通常用于 TCP 的紧急数据机制。
EPOLLERR:表示对应的文件描述符发生了错误,例如,读写操作失败或者遇到了网络错误。
EPOLLHUP:表示对应的文件描述符被挂断。对于一个 socket,这通常意味着对端关闭了连接,并且不会再有数据到来。
EPOLLET:将 epoll 设置为边缘触发模式(Edge Triggered)。边缘触发模式只会在状态变化时通知一次,即只有在状态发生变化时才会报告事件。与水平触发模式(Level Triggered)相比,边缘触发模式可能需要更频繁地检查文件描述符的状态,因为它不会在状态保持不变的情况下重复报告事件。(下方会详细说)
EPOLLONESHOT:表示只监听一次事件。设置了这个标志的文件描述符在事件触发后会从 epoll 实例中移除,直到你手动将它重新添加到 epoll 实例中。这是为了处理事件后重新注册,以防止在事件处理过程中丢失事件。适用于需要在每次事件发生后都重新注册的场景,确保事件处理的健壮性。
epoll_data是一个联合体,在某些情况下,你可能只需要其中一种东西,当然我们可以看到其中有一个指针参数,这更是加大了灵活性,比如
struct connection_info {
int fd;
// 其他与连接相关的数据
};
struct connection_info *conn_info = new connection_info();
conn_info->fd = conn_fd;
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLRDHUP;
ev.data.ptr = conn_info;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
perror("epoll_ctl: conn_fd");
close(conn_fd);
delete conn_info;
}
//在处理事件时,你可以通过 events[n].data.ptr 访问该指针,并获取结构体中的信息:
struct connection_info *conn_info = static_cast<connection_info*>(events[n].data.ptr);
int fd = conn_info->fd;
// 处理连接的读写等事件
这样的处理可以增强灵活性、空间节省、便于传递数据。
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明
epfd
:epoll
实例的文件描述符,是之前调用epoll_create
或epoll_create1
函数时获得的。events
:一个指向epoll_event
结构体数组的指针,用于存储返回的事件列表。epoll_wait
将填充这个数组,数组的大小由maxevents
参数指定。maxevents
:events
数组的大小,即最大可以返回的事件数量。epoll_wait
可能返回的事件数量最多为maxevents
,实际返回的数量由事件发生的数量决定。timeout
:等待事件的超时时间,以毫秒为单位。如果设置为-1
,epoll_wait
将会阻塞,直到至少一个事件发生。如果设置为0
,epoll_wait
会立即返回,适用于非阻塞检查。如果设置为正数,epoll_wait
会等待指定的时间后返回,适用于有超时要求的场景。
返回值
- 成功时,返回发生的事件数量。这个数量可能小于或等于
maxevents
。- 如果没有事件发生并且
timeout
为0
,返回0
。- 失败时,返回
-1
并设置errno
。
其中events是一个输出型参数,epoll_wait
会在返回时填充 events
数组,数组的前 nfds
个元素会包含发生的事件。nfds
是 epoll_wait
返回的事件数量,表明数组中有多少个有效的事件。
处理事件:遍历 events
数组,根据 events[n].events
的值来识别事件类型,并执行相应的处理逻辑。events[n].data
包含与事件相关的文件描述符或其他数据。
epoll的使用
总结一下, epoll的使用过程就是三部曲:
调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪;
epoll工作原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关.
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
- 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
- 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
- 在epoll中,对于每一个事件,都会建立一个epitem结构体.
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
epoll的优点(和 select 的缺点对应)
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
- 没有数量限制: 文件描述符数目无上限
epoll工作方式
你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式:
- 如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次…(亲妈, 水平触发)
- 如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发)
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
假如有这样一个例子:
我们已经把一个tcp socket添加到epoll描述符
这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
然后调用read, 只读取了1KB的数据
继续调用epoll_wait…
水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait
仍然会立刻返回并通知socket读事件就绪.
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
支持阻塞读写和非阻塞读写
边缘触发Edge Triggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
- 当epoll检测到socket上事件就绪时, 必须立刻处理.
- 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了.
- 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
- 只支持非阻塞的读写
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET.
对比LT和ET
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高了.
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET