从select的一个死循环谈epoll的ET模式
最近写程序遇到一个问题,就是发现select监听标准输出的时候遇到了死循环,具体程序如下程序一。程序的意图是每当用户在控制台有任何输入,就输出”hello world!”。
程序一:
#include
#include
#include
#include
int
main(int argc, char *argv[])
{
int maxfdp1;
char buf[256];
fd_set rset;
maxfdp1=STDIN_FILENO+1;
for(;;)
{
FD_ZERO(&rset);
FD_SET(STDIN_FILENO,&rset);
select(maxfdp1,&rset,NULL,NULL,NULL);
if(FD_ISSET(STDIN_FILENO,&rset))
{
printf("hello world!\n");
}
}
return 0;
}
运行结果:
结果会循环输出“hello world!”,这是为什么呢?也就是当我们输入任意字符后,select每次都判断标准输入的描述符就绪。造成这种情况的原因要从select的机制说起。如下图所示:
没一个文件描述符(fd)与一个缓冲关联,select对fd的监听其实就是监听fd的缓冲,当缓冲中有数据要读的时候,select就认为该fd可读就绪,当缓冲中有数据待写的时候,select就认为该fd可写就绪。
下面我们在分析一下我们的程序一,当输入任意字符,比如:“abc”,则“abc”被放在标准输入的缓冲当中,此时缓冲中有数据(abc)待读,所以select返回STDIN_FILENO就绪,程序输出“hello World!”。紧接着进入下一次循环,select重新将STDIN_FILENO加入监听的描述符集,由于刚刚的“abc”并没有被读出,所以仍在缓冲中,此时STDIN_FILENO的缓冲中仍有数据等待读,所以select又返回STDIN_FILENO可读就绪,又一次输出“hello world!”。
之后循环情况类似,由于缓冲的的“待读”数据始终还在,所以每次select都直接返回STDIN_FILENO就绪,每次都输出“hello world!”,这就是造成程序死循环的原因。那么让偶们如何解决呢?
方法一:将缓冲区中的“待读”数据读出,程序修改如下所示:
if(FD_ISSET(STDIN_FILENO,&rset))
{
read(STDIN_FILENO,buf,sizeof(buf));//将缓冲区的数据读出(读入buf数组)
printf("hello world!\n");
}
修改后程序正常运行(任意输入后,输出“hello world!”)。我们在做如下实验——输入多个字符,制度出部分字符。修改程序做如下修改:
char buf[2];//将buf长度改为2,每次从缓冲区读入两个字符
程序运行结果如下:
分析:
(1) 输入一个字符’s’,s被放入缓冲区,同时放入缓冲区中的还有换行符’\n’,缓冲区中有待读数据,select返回读就绪,read将’s\n’读出,缓冲区清空,select再次阻塞。
(2) 输入两个”dd”,缓冲区中的数据变为”dd\n”,select返回读就绪,read读出两个字符——”ss”,输出hello world!此时缓冲区中还有’\n’,所以下一次select依然返回读就绪,之后read将’\n’读出,输出hello world!,缓冲区清空,select阻塞。
(3) 之后输入三个字符,四个字符的情况类似,不在分析。
到此,我们将程序一基本分析清楚,但我们的讨论远没有结束。因为,这个程序的现象令我想起了另一个知识点——epoll的LT和ET模式。
关于ET和LT模式的介绍,之前的博文已经写得很详细了,这里不再重复。这里想说的是,以上程序一的现象正式LT模式的一个典型实例,也是LT模式的一个缺陷。我们知道select和poll都是采用LT模式,并且只有这一种模式。所以,使用select或者poll要想解决程序一的问题只能采用方法一。下面我们采用epoll的ET模式解决,也就是方法二。代码如下。
程序二:
#include
#include
#include
using namespace std;
int main(void)
{
int epfd,nfds;
struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件
epfd=epoll_create(1);//只需要监听一个描述符——标准输入
ev.data.fd=STDIN_FILENO;
ev.events=EPOLLIN|EPOLLET;//监听读状态同时设置ET模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev);//注册epoll事件
for(;;)
{
nfds=epoll_wait(epfd,events,5,-1);
for(int i=0;i
{
if(events[i].data.fd==STDIN_FILENO)
cout<<"hello world!"<
}
}
}
运行结果:
可以发现,使用ET模式,程序正常运行,虽然输入缓冲区的数据并没有被读出,但是只要没有新的数据进入,epoll就不再被通知(只被通知一次),当再次输入数据,又有新的数据进入缓冲时才会触发epoll,再次返回读就绪,输出”hello world!”。
我们再看看使用LT模式的情况,将程序二以下修改:
ev.events=EPOLLIN;//默认使用LT模式
运行结果:
可以发现和select的结果一样。