麻了,对 epoll 的触发机制理解不深刻…面试又被拷打了…
下面总结一下各种情况,并不涉及底层原理,底层原理看这里。
文章结构可以看左下角目录、
有什么理解的不对的,请大佬们指点。
先说结论,下面再验证:
-
LT(水平触发):
-
EPOLLIN 触发条件:
读缓冲区有数据就一直触发(即epoll_wait时能检测到),没有就不触发。 -
EPOLLOUT 触发条件:
写缓冲区有空间可写,则一直触发。
-
-
ET(边缘触发)
-
EPOLLIN 触发条件:
1. 当读 buff 从 空 -> 不空 时,触发;
2. 当有新数据到达时,即读 buff 数据由 少 -> 多 时,触发;
3. 当读 buff 有数据可读时,我们不处理,但是对相应fd进行epoll_mod IN事件时,触发。 -
EPOLLOUT 触发条件:
1. 当写 buff 从 满 -> 不满 时,触发;
2. 当有数据被送走时,即写 buff 数据由 多 -> 少 时,触发;
3. 当写 buff 有数据,但是我们没处理(没发送出去),但是对相应fd进行epoll_mod OUT事件时,触发。
-
可以简单总结就是:
LT模式: 读buff有数据 / 写buff有空间,就触发;
ET模式: 读buff有数据,且数据减少 或 MOD时 / 写 buff 空间增加或MOD时,才触发。
验证:
下面将从几方面验证上面的结论:
LT模式
检测EPOLLIN
不读出数据
读出数据
检测EPOLLOUT
不刷新缓冲区
刷新缓冲区
ET模式
检测EPOLLIN(1)
不读出数据
读出数据
检测EPOLLIN(2,用MOD)
不读出数据
读出数据
检测EPOLLOUT(1)
不刷新缓冲区
刷新缓冲区
检测EPOLLOUT(2,用MOD)
不刷新缓冲区
刷新缓冲区
LT模式:
第一种:LT模式,检测EPOLLIN,未将数据读出
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDIN_FILENO; //接受键盘输入
ev.events = EPOLLIN; //默认就是LT模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev); //监听键盘输入文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDIN_FILENO)
{
cout<<"LT 模式下 EPOLLIN,未将数据读出\n";
sleep(1);
}
}
return 0;
}
结果:
输入test回车后,死循环;
证明socket读缓冲区有数据,则会一直触发EPOLLIN
第二种:LT模式,测试EPOLLIN,将数据读出
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDIN_FILENO; //接受键盘输入
ev.events = EPOLLIN; //默认就是LT模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev); //监听键盘输入文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDIN_FILENO)
{
char buff[128];
read(STDIN_FILENO,buff,sizeof(buff));
cout<<"LT 模式下 EPOLLIN,读出数据\n";
sleep(1);
}
}
return 0;
}
结果:
当我们将数据读出来后,EPOLLIN不触发了;
即socket读缓冲区没数据,不触发EPOLLIN。
第三种:LT模式,测试EPOLLOUT,不刷新缓冲区
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDOUT_FILENO; //检测输出缓冲区
ev.events = EPOLLOUT; //默认就是LT模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev); //监听输出文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDOUT_FILENO)
{
/*
这里需要清楚一个点,我们检测的是标准输出:
用cout,不加\n的话,缓冲区不会刷新,仍然有数据;
*/
cout<<"LT模式,测试EPOLLOUT,不刷新缓冲区-----";
// sleep(1); //这里不能sleep,不然会一直阻塞住...不懂
}
}
return 0;
}
结果:死循环了
非常合理,没有将写缓冲区刷新,写缓冲区有空间可写则一直触发EPOLLOUT,当buffer满的时候,buffer会自动刷清输出,同样会造成epoll_wait返回写就绪。
第四种:LT模式,测试EPOLLOUT,刷新缓冲区(加一个endl)
cout<<"LT模式,测试EPOLLOUT,不刷新缓冲区-----"<<endl;
结果:
同样死循环
非常合理,刷新缓冲区后,写缓冲区有空间可写,则一直触发。
——————————————————————————————————————————————————————
ET模式:
第五种:ET模式,测试EPOLLIN;
(1)不将读缓冲区数据读出
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDIN_FILENO; //检测输入缓冲区
ev.events = EPOLLIN | EPOLLET; //测试ET模式下
epoll_ctl(epfd,EPOLL_CTL_ADD,EPOLLIN,&ev); //监听输入文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDIN_FILENO)
{
cout<<"ET模式,测试EPOLLIN,不将缓冲区数据读出\n";
// sleep(1);
}
}
return 0;
}
结果:
输入一个test,输出一下,然后阻塞了,再输入再输出,然后阻塞;
分析流程:
1. 一开始读缓冲区为空,阻塞;输入test,即将test送入读缓冲区,此时由 空 -> 不空,触发EPOLLIN。(合理,验证上面的结论)
2. 触发EPOLLIN后,由于我们没对缓冲区处理,此时不会一直触发,即调用epoll_wait被阻塞住了。
3. 当我们再次输入test时,读缓冲区数据增加,导致fd状态改变,此时也会触发EPOLLOUT,因此epoll_wait返回,再次输出。(合理,验证上面 读缓冲区 增加数据时 也会触发的结论)
(2)那么如果我们将读缓冲区的数据读出来呢?
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDIN_FILENO)
{
char buff[128];
read(STDIN_FILENO,buff,sizeof(buff));
cout<<"ET模式,测试EPOLLIN,读出读缓冲区数据\n";
// sleep(1);
}
结果:
和不读出来是一样的,并不会出现死循环;
说明ET模式下,读缓冲数据 减少 / 非空->空 并不会触发EPOLLIN。
第六种:ET模式下,测试EPOLLIN,用 EPOLL_MOD 重置
(1)不将 读缓冲区 数据读出:
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDIN_FILENO; //检测输入缓冲区
ev.events = EPOLLIN | EPOLLET; //测试ET模式下
epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev); //监听输入文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDIN_FILENO)
{
cout<<"ET模式,测试EPOLLIN,用EPOLL_MOD重置,不读出缓冲区\n";
ev.data.fd = STDIN_FILENO; //检测输入缓冲区
ev.events = EPOLLIN | EPOLLET; //测试ET模式下
epoll_ctl(epfd,EPOLL_CTL_MOD,STDIN_FILENO,&ev); //重新MOD事件(ADD无效)
// sleep(1);
}
}
return 0;
}
结果:
出现死循环(非常合理,验证了上面的结论,缓冲区非空,用EPOLL_MOD重置,会触发)
(2)那如果将读缓冲区数据读出来呢?
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDIN_FILENO)
{
cout<<"ET模式,测试EPOLLIN,用EPOLL_MOD重置,读出读缓冲区数据\n";
char buff[128];
read(STDIN_FILENO,buff,sizeof(buff));
ev.data.fd = STDIN_FILENO; //检测输入缓冲区
ev.events = EPOLLIN | EPOLLET; //测试ET模式下
epoll_ctl(epfd,EPOLL_CTL_MOD,STDIN_FILENO,&ev); //重新MOD事件(ADD无效)
// sleep(1);
}
结果:
输入一次test,输出一次,也就是说 当读缓冲区为空,用EPOLL_MOD并不会一直触发。
第七种:ET模式,测试EPOLLOUT;
(1)不刷新缓冲区
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDOUT_FILENO; //检测输出缓冲区
ev.events = EPOLLOUT | EPOLLET; //测试ET模式下,EPOLLOUT
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev); //监听输出文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDOUT_FILENO)
{
cout<<"ET模式,测试EPOLLOUT,不读出缓冲区";
// sleep(1);
}
}
return 0;
}
结果:
直接就阻塞了…
分析:第一次EPOLLOUT触发epoll_wait返回,然后我们往写缓冲区写数据,但是我们没刷新,此时缓冲区中有数据,此时当我们再次epoll_wait时,EPOLLOUT不触发,因此阻塞了。(非常合理,ET模式下,事件触发后不处理,下次不再触发)
(2)刷新缓冲区
cout<<"ET模式,测试EPOLLOUT,刷新缓冲区"<<endl;
结果:
死循环了,非常合理,验证了上面结论,ET模式下,写缓冲区数据由多变少时,会触发。
第八种:ET模式,测试EPOLLOUT,用EPOLL_MOD重置
(1)不刷新缓冲区
#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main()
{
int epfd = epoll_create(1); //创建epoll实例
struct epoll_event ev,event[5];
//设置参数
ev.data.fd = STDOUT_FILENO; //检测输出缓冲区
ev.events = EPOLLOUT | EPOLLET; //测试ET模式下,EPOLLOUT
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev); //监听输出文件描述符
while(1)
{
int nfd = epoll_wait(epfd,event,5,-1);
for(int i=0;i<nfd;i++)
if(event[i].data.fd == STDOUT_FILENO)
{
cout<<"ET模式,测试EPOLLOUT,用EPOLL_MOD重置,不刷新写缓冲区";
ev.data.fd = STDOUT_FILENO; //检测输入缓冲区
ev.events = EPOLLOUT | EPOLLET; //测试ET模式下
epoll_ctl(epfd,EPOLL_CTL_MOD,STDOUT_FILENO,&ev); //重新MOD事件(ADD无效)
}
}
return 0;
}
结果:
死循环,非常合理,验证了上面的,写缓冲区可写时,用EPOLL_MOD重置后会触发EPOLLOUT
(2)刷新缓冲区
cout<<"ET模式,测试EPOLLOUT,用EPOLL_MOD重置,刷新写缓冲区"<<endl;
结果:
同样死循环,非常合理,刷新之后写缓冲区依然可写,EPOLL_MOD重置后,触发EPOLLOUT
总结
-
LT(水平触发):
-
EPOLLIN 触发条件:
读缓冲区有数据就一直触发(即epoll_wait时能检测到),没有就不触发。 -
EPOLLOUT 触发条件:
写缓冲区有空间可写,则一直触发。
-
-
ET(边缘触发)
-
EPOLLIN 触发条件:
1. 当读 buff 从 空 -> 不空 时,触发;
2. 当有新数据到达时,即读 buff 数据由 少 -> 多 时,触发;
3. 当读 buff 有数据可读时,我们不处理,但是对相应fd进行epoll_mod IN事件时,触发。 -
EPOLLOUT 触发条件:
1. 当写 buff 从 满 -> 不满 时,触发;
2. 当有数据被送走时,即写 buff 数据由 多 -> 少 时,触发;
3. 当写 buff 有数据,但是我们没处理(没发送出去),但是对相应fd进行epoll_mod OUT事件时,触发。
-
参考文章
彻底学会使用epoll(一)——ET模式实现分析
彻底学会使用epoll(二)——ET的读写操作实例分析
epoll LT/ET 深度剖析
深度剖析linux socket的epollin/epollout是何时触发的
我是一个找暑期实习的鼠鼠,今天是面完美团第二天,团子收了我吧!!!