目录
(1)水平触发(Level-Triggered,LT)场景介绍:
(2)边沿触发(Edge-Triggered,ET)场景介绍:
1. epoll概要
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本。它能够显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
2. 优点
支持大数目的文件描述符:与select相比,epoll不受文件描述符数量的限制。select支持的进程描述符由FD_SETSIZE设置,默认值为1024,而epoll可以支持的最大文件描述符数目通常远大于这个值,具体数目取决于系统内存大小。
效率不会随监听socket数目的增加而线性下降:select采用轮询的方式扫描所有的socket集合,如果socket数量过大且大多数处于idle状态,select的效率会非常低。而epoll只会对活跃的socket进行操作,因此在处理大量并发连接时,epoll的效率远高于select。
提供边缘触发(Edge Triggered)和水平触发(Level Triggered)两种模式:边缘触发模式只在状态发生变化时通知一次,适用于需要高性能的场景;水平触发模式则只要事件未处理就会持续通知,适用于处理流式数据。
3. 工作模式及API
(1)工作模式
epoll主要通过三个系统调用来实现其功能:epoll_create、epoll_ctl和epoll_wait。
epoll_create:创建一个epoll实例,并返回一个文件描述符,用于后续操作。
epoll_ctl:用于向epoll实例中添加、修改或删除需要监听的文件描述符及其事件。操作类型包括EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)和EPOLL_CTL_DEL(删除)。
epoll_wait:等待并返回发生在被监听文件描述符上的事件。用户可以通过设置超时时间来控制等待的时间。
(2)API介绍
int epoll_create(int size)
当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(也就是程序中 epfd 所代表的对象)。eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等待队列。
参数理论上没有意义,但是会与epoll_wait时的maxevents相联动,maxevents不能超过此size值。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
arg1:epoll_create()的返回值。
arg2:第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
arg3:需要监听的fd。
arg4:告诉内核需要监听什么事
events可以以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
收集在epoll监控的事件中已经发送的事件。
参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
需要注意,所有注册事件的描述符,用户都需要明确其用途,如指令通道socket描述符用来接收指令,流媒体数据通道描述符用来接收流媒体数据。在捕获到事件时,需要对应分支进行操作.
4. 触发类型
(1)LT(水平触发,默认)
只要缓冲区有数据,epoll_wait就会一直被触发,直到缓冲区为空;
LT是默认的工作模式,对于采用LT的文件描述符,在水平触发模式下,如果某个文件描述符上有数据可读,内核会持续通知应用程序,直到应用程序处理完数据或者缓冲区不再有数据可读为止。当调用epoll_wait检测到其上有事件发生并通知应用程序时,应用程序可以不立即处理完毕该事件。这样,当程序下一次调用epoll_wait时,epoll_wait还会向应用程序通知此事件,直到事件被处理完毕
应用场景:这种机制适用于处理流式数据,如TCP连接,其中数据可能被分成多个部分到达,需要在多次epoll_wait调用之间保持对同一文件描述符的关注。,典型的如解析http报文,分次解析http报文。
(2)ET(边缘触发,EPOLLET)
只有所监听的事件状态改变或者有事件发生时,epoll_wait才会被触发.
ET比LT效率高,对于使用ET模式的文件描述符,在边缘触发模式下,只有在文件描述符状态变化(对于EPOLLIN事件,只有在状态从未就绪变化为就绪,才叫做变化)发生时才会触发事件,所以调用epoll_wait检测到其上有事件发生,并通知应用程序,应用程序必须立即处理完毕该事件,否则会造成数据丢失,因为后续的epoll_wait调用不再重复向应用程序通知此事件
应用场景:ET模式适用于处理消息队列或特定事件,因为它只在状态变化时通知,减少了不必要的事件通知,通常可以提供更高的性能。读取大型文件,用while循环一次全部读取完毕
(3)EPOLLONESHOT(一次边沿触发)
即使使用了ET模式,一个socket上的事件还是可能被触发多次。例如:当一个线程处理一个socket时有新数据写入,此时另外一个线程被唤醒读取这些数据,于是出现了两个线程同时操作一个socket的情况。EPOLLONESHOT解决了多个线程同时操作一个socket的问题,对于注册了EPOLLONESHOT的事件,操作系统最多触发其上注册的一个可读可写异常事件,且只触发一次。这样,一个线程在操作这个socket时,其他线程不可能有机会操作该socket。
注册了EPOLLONESHOT事件的socket,一旦被某个线程处理完毕,要及时修改为EPOLLIN或其他事件,以确保下次这个socket可读时,其事件能够被触发,进而让其他线程有机会继续处理。
5. EPOLLOUT
EPOLLOUT 是 Linux 系统中 epoll 事件的一种类型,用于标识文件描述符(通常是套接字)已经准备好进行写操作。当一个文件描述符缓冲区中的数据可以被成功写出时,EPOLLOUT 事件就会被触发。
EPOLLOUT与LT配合使用,满足以下两种情况时,epoll_wait会返回可写事件:
(1)当一个文件描述符准备好进行 I/O 操作时(例如,套接字的发送缓冲区有空余),它会产生一个事件。
(2)如果应用程序没有完全消费这个事件(例如,没有将发送缓冲区填满),该文件描述符仍然会持续产生事件,直到应用程序采取了适当的行动。
EPOLLOUT与ET配合使用情况比较特殊,一般是“一次性通知”,即只在可写时进行一次通知,之后不会再进行通知。如有需要,需再次进行事件的注册。
6. 当读取数据时,有新数据到来的情况。
ET模式:一个文件描述符上有数据可读(EPOLLIN触发),线程开始处理数据,而在处理过程中又有新数据加入。当旧数据开始处理时,文件描述符仍然保持在就绪状态。但当有新的数据写入时,文件描述符会从就绪状态变为未就绪状态,然后再次变为就绪状态,触发一次新的 EPOLLIN 事件。这样可以确保即使在旧数据处理过程中有新的数据写入,应用程序也能及时地得到通知,并读取新的数据。在单线程上,此时只要程序循环读取数据就不会造成新数据的丢失,而对于多线程就需要特殊处理(详见下EPOLLONESHOT)
LT 模式:新数据的写入不会改变文件描述符的状态码。如果文件描述符上有数据可读,它的状态码会一直保持就绪状态,直到所有的数据都被读取完毕才会变为未就绪。即使在读取数据的过程中有新的数据写入,文件描述符的状态码仍然会保持就绪状态,不会因为新数据的写入而改变。
7. 使用过程中的若干问题
(1)为什么一般要配合非阻塞使用?
一般情况下,ET强烈建议使用非阻塞,LT则不强制使用。
当缓冲区中的数据不足以满足一次read调用时,read调用会阻塞,导致应用程序无法继续处理其他I/O事件,甚至可能错过重要数据。
(2)什么情况下,LT可以配合阻塞I/O?
当应用程序需要处理大量数据或流式数据时(如TCP连接中的数据流),水平触发模式可以持续通知应用程序文件描述符上的可读或可写事件,直到所有相关事件都被处理完毕。在这种情况下,即使使用阻塞I/O,应用程序也可以在处理完当前文件描述符上的事件后,继续处理下一个就绪的文件描述符,因为epoll会确保只要事件就绪,就会持续通知。
虽然水平触发模式可以配合阻塞I/O使用,但在某些情况下,使用非阻塞I/O可能会获得更好的性能。非阻塞I/O允许应用程序在等待I/O操作完成时继续执行其他任务,从而提高并发性能。
8. 应用场景
简单讲就是按照小流大块来划分。
小流,即小数据包,流式传输,推荐使用LT触发模式。
大块,大数据量,块式传输,推荐使用ET触发模式。
(1)水平触发(Level-Triggered,LT)场景介绍:
水平触发模式适用于需要确保数据完整性和处理少量数据的场景。在这种模式下,只要文件描述符上的事件(如可读、可写)处于就绪状态,epoll就会持续通知应用程序,直到该事件被处理完毕。这种机制特别适合于处理流式数据,如TCP连接中的数据流,因为数据可能会分多个部分到达,需要在多次epoll_wait调用之间保持对同一文件描述符的关注。
详细介绍
持续通知:在LT模式下,只要文件描述符上的事件就绪,epoll就会持续通知应用程序,即使事件未被完全处理。这确保了应用程序有足够的机会来读取或写入数据,从而避免数据丢失。
适用于流式数据:由于TCP连接中的数据是以流的形式传输的,可能会分多个TCP包到达,因此LT模式非常适合于处理这种情况。应用程序可以在多次epoll_wait调用中逐步读取数据,直到整个消息被完整接收。
可能的问题:如果应用程序在处理事件时不够高效,可能会导致epoll事件队列中积压大量事件,进而影响性能。此外,如果文件描述符一直处于就绪状态(如对方持续发送数据),epoll将不断通知应用程序,这可能会浪费CPU资源。
(2)边沿触发(Edge-Triggered,ET)场景介绍:
边沿触发模式适用于高并发、高吞吐量的场景,特别是当需要精确控制事件通知次数时。在这种模式下,epoll只在文件描述符的状态发生变化时通知应用程序一次。这要求应用程序在收到通知后必须尽可能多地处理数据,因为下次通知可能不会到来,除非有新的状态变化发生。
详细介绍
单次通知:在ET模式下,epoll只在文件描述符的状态从不可读变为可读(或从不可写变为可写)时通知应用程序一次。如果应用程序没有在一次通知中处理完所有数据,它需要在下次epoll_wait调用之前继续处理剩余的数据,因为下次通知可能不会到来。
高性能:由于ET模式减少了不必要的通知次数,因此可以显著提高应用程序的性能和吞吐量。它特别适合于需要处理大量并发连接的场景,如Web服务器、数据库服务器等。
编程复杂性:然而,ET模式也增加了编程的复杂性。应用程序需要确保在收到通知后能够尽可能多地处理数据,并且需要妥善处理数据残留问题(即在一次通知中未处理完的数据)。此外,ET模式通常与非阻塞I/O结合使用,以进一步提高性能。
(3)流媒体应用场景下,如何选择触发方式?
一般情况下可以这样设定,传输指令使用ET模式,传输流数据使用LT模式。
9. 例程
#include <sys/socket.h> //for socket
#include <arpa/inet.h> //for htonl htons
#include <sys/epoll.h> //for epoll_ctl
#include <unistd.h> //for close
#include <fcntl.h> //for fcntl
#include <errno.h> //for errno
#include <iostream> //for cout
class fd_object
{
public:
fd_object(int fd) { listen_fd = fd; }
~fd_object() { close(listen_fd); }
private:
int listen_fd;
};
int main(int argc, char* argv[])
{
/** 创建socket. */
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1){
std::cout << "Create socket failed." << std::endl;
return -1;
}
fd_object obj(listen_fd);
/** 设置为非阻塞. */
int socket_flag = fcntl(listen_fd, F_GETFL, 0);
socket_flag |= O_NONBLOCK;
if (fcntl(listen_fd, F_SETFL, socket_flag) == -1)
{
std::cout << "Set listen fd to nonblock failed." << std::endl;
return -1;
}
/** Bind。 */
int port = 51741;
struct sockaddr_in bind_addr;
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(port);
if (bind(listen_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) == -1){
std::cout << "Bind socket fd failed." << std::endl;
return -1;
}
/** Listen。 */
if (listen(listen_fd, SOMAXCONN) == -1){
std::cout << "Listen error." << std::endl;
return -1;
}else{
std::cout << "Start server at port [" << port << "] with [" << (argc <= 1 ? "LT" : "ET") << "] mode." << std::endl;
}
/** 创建Epoll. */
int epoll_fd = epoll_create(88);
if (epoll_fd == -1)
{
std::cout << "Create epoll." << std::endl;
return -1;
}
/** 添加Epoll事件. */
epoll_event listen_fd_event;
listen_fd_event.data.fd = listen_fd;
listen_fd_event.events = EPOLLIN;
if (argc > 1) listen_fd_event.events |= EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_fd_event) == -1){
std::cout << "Add epoll event failed." << std::endl;
return -1;
}
/** 循环处理. */
while (true){
epoll_event epoll_events[1024]; ///< 最多收集1024个epoll事件.
int n = epoll_wait(epoll_fd, epoll_events, 1024, 1000);
if (n < 0)
break;
else if (n == 0) ///< 超时,没有捕获事件
continue;
for (int i = 0; i < n; ++i) ///< 依次处理事件.
{
if (epoll_events[i].events & EPOLLIN)//trigger read event
{
if (epoll_events[i].data.fd == listen_fd){
/** 服务端可读事件. */
/** 接受新连接. */
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1)
continue;
socket_flag = fcntl(client_fd, F_GETFL, 0);
socket_flag |= O_NONBLOCK;
if (fcntl(client_fd, F_SETFL, socket_flag) == -1){
close(client_fd);
std::cout << "Set client fd to non-block failed." << std::endl;
continue;
}
/** 注册客户端fd的可读、可写事件. */
epoll_event client_fd_event;
client_fd_event.data.fd = client_fd;
client_fd_event.events = EPOLLIN | EPOLLOUT;
if (argc > 1) client_fd_event.events |= EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_fd_event) == -1){
std::cout << "Add client epoll event(r/w) failed." << std::endl;
close(client_fd);
continue;
}
std::cout << "Accept a new client fd [" << client_fd << "]." << std::endl;
}else{
/** 客户端可读事件. */
std::cout << "Client EPOLLIN event triggered, fd [" << epoll_events[i].data.fd << "]." << std::endl;
char recvbuf[1024] = { 0 };
int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggered
if (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." << std::endl;
close(epoll_events[i].data.fd);
}
std::cout << "Recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}
}else if (epoll_events[i].events & EPOLLOUT){
if (epoll_events[i].data.fd == listen_fd) /// 不处理监听fd的写操作
continue;
/** 客户端可写事件. */
std::cout << "Client EPOLLOUT event triggered, fd [" << epoll_events[i].data.fd << "]." << std::endl;
}
}
}
return 0;
}