epoll有两种工作方式,一种是LT模式(level trigger,水平触发),一种是ET模式(edge trigger,边缘触发)。默认情况下是LT模式。
目录
1、认识epoll模型的工作方式
(1) LT模式
以读事件为例,当缓冲区有数据准备好的时候,此时会触发读事件,如果我们一直不去读取缓冲区里的数据,epoll模型就会一直通知我们有事件就绪,即epoll_wait中的events参数就会一直包含某个文件描述符的读事件。
这就是LT模式,也是epoll模型的默认模式。
(2) ET模式
ET模式与LT模式相反,当缓冲区就数据准备好的时候,也会触发读事件,但是只会触发一次,如果我们这次没有调用read/recv读取 或者 没有一次读完,后面就不会通知有读事件就绪了。简单来说,只有当缓冲区里的数据量发生变化的时候,才会通知我们一次,不会像LT模式那样一直通知。
这就是ET模式,单纯的从通知效率这个角度来看,ET模式的效率更高,因为不会重复通知某一个事件就绪。
2、ET模式必须将文件描述符设为非阻塞的原因
既然这是一个必要条件,那必然存在对应的情景来要求我们将文件描述符设为非阻塞模式。
(1) 情景介绍
客户端给服务端发送了一个大小为100K byte的数据,读事件就绪,epoll模型通知你一次,但是服务端一次没有读完,还剩余50K在缓冲区里。等到下一次调用epoll_wait的时候,由于epoll是ET模式,已经通知过你一次,这次就不认为sock有读事件就绪,epoll_wait 也就不会返回该文件描述符上读事件就绪的信息。
因此,我们需要将数据一次读完,那么这跟设为非阻塞模式有什么关系?下面细说。
(2) 原因分析
上面提到,我们需要将数据一次读完,因此我们需要循环读取数据,就像下面这样。
假设每次读取1024个字节,等读完这100K个字节时,recv发现缓冲区里没有数据了,此时会默认进入阻塞状态,等待数据就绪,这就严重影响到后面的文件描述符读取/写入内容了。所以ET模式下必须要设为非阻塞。
(3) 解决方案(设为非阻塞的方法)
如果使用recv函数读取数据
一般我们可以设置recv的最后一个参数来设为非阻塞模式,但是因为非阻塞模式下,没有数据可读的时候,返回0,这就跟对端关闭连接一样,不容易区分。
因此这里我们采用的是通过recv返回的字节大小判断是否全部读完。现在数据量的大小是100K字节,每次读取1024个字节,最后一次读取的时候,只有672个字节,说明这是最后一批数据了,读完这批数据就可以直接跳出循环了。
#define SIZE 1024
char recvBuffer[SIZE] = {0};
for(;;)
{
ssize_t s = recv(sock, recvBuffer,sizeof(recvBuffer)-1,0);
if(s > 0){
// 其他处理操作...
if(s <= SIZE){
//说明这是最后一批数据了
break;
}
}
}
如果使用的是read读取数据
如果使用的是read读取数据,我们可以在读取数据之前使用fcntl 接口来将文件描述符设为非阻塞模式,下面是已经封装好的函数。
bool SetNoBlock() {
int fl = fcntl(fd_, F_GETFL); //获取之前的状态信息
if (fl < 0) {
perror("fcntl F_GETFL");
return false;
}
int ret = fcntl(fd_, F_SETFL, fl | O_NONBLOCK); //在之前状态信息的基础上添加非阻塞状态
if (ret < 0) {
perror("fcntl F_SETFL");
return false;
}
return true;
}
3、使用ET模式的两个要求
要求一:必须要一次读完/写入所有的数据。因为ET模式只会通知一次,下一次读取只能是缓冲区接收到了新的数据。
要求二:必须设为非阻塞模式。循环读取的时候,如果缓冲区没有数据或者低于水位线,recv/read就会阻塞等待读事件就绪,这会影响到epoll模型中其他文件描述符的操作。
4、总结
LT 模式(水平触发,默认)只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回
ET模式(边缘触发)只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回;