您的位置:首页 > 其它

epoll的边缘触发(ET)和水平触发(LT)

2017-05-13 11:30 253 查看

epoll的边缘触发和水平触发:

epoll的默认模式是水平触发。

先大概了解一下这两种触发模式有什么不同:

水平触发(Level Trigger,也称条件触发):只要满足条件,就触发一个事件(只要有数据还未读完,就会一直触发)

边缘触发(Edge Trigger):每当状态发生变化时就触发一个事件。

可能概念不容易理解,这里举一个例子大概就能明白两者的区别了:比如某个人让你去买几袋酱油,你只买了一袋回去,水平触发的做法就是他让你继续去把剩下的几袋酱油买回来,如果没有完成任务,就一直通知你;边缘触发的做法就是不管完没完成任务,反正他让你买了,买没买完就是你自己的事了,下次买酱油这件事他就不管了,会让你去做其它的事。

通过上面的例子,我们对边缘触发和水平触发有了一个大概的了解,下面通过代码来深入了解:

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>

#define MAXLINE 10

int main()
{
pid_t pid;
int fd[2];
int i;
char str[MAXLINE], ch = 'a';
bzero(str, sizeof(str));
//使用管道,fd[0]默认是读端,fd[1]默认是写端
pipe(fd);
pid = fork();
if(pid == 0)    //child 负责写端
{
close(fd[0]);
while(1)
{
for(i = 0; i < MAXLINE / 2; i++)
{
str[i] = ch;
}
ch++;
str[i - 1] = '\n';
for(; i < MAXLINE; i++)
{
str[i] = ch;
}
str[i - 1] = '\n';

write(fd[1], str, sizeof(str));
sleep(5);
}

}
else if(pid > 0) //parent 负责读端
{
close(fd[1]);
struct epoll_event event;
struct epoll_event resevent[10];

int res;
//调用epoll_create创建红黑树树根
int efd = epoll_create(10);
event.data.fd = fd[0];
//触发方式默认是EPOLLLT
event.events = EPOLLIN;
//将读端文件描述符加入epoll监听的树中
epoll_ctl(efd, EPOLL_CTL_ADD, fd[0], &event);

while(1)
{
//当子进程发送数据时,就会触发事件进行读事件
res = epoll_wait(efd, resevent, 10, -1);
if(resevent[0].data.fd == fd[0])
{
int len = read(fd[0], str, MAXLINE/2);
//写回屏幕
write(STDOUT_FILENO, str, len);
}
}
close(efd);
}
if(pid > 0)
close(fd[0]);
else
close(fd[1]);
return 0;
}


这段程序运行的结果是:

aaaa
bbbb
bbbb
cccc
....


当我们修改成边缘触发(这里就不贴出完整代码了,因为改动很少),只需要把
event.events = EPOLLIN;
改成
event.events = EPOLLIN | EPOLLET
即可。


运行的结果是:

aaaa
bbbb
bbbb
cccc
....


虽然结果是一样的,但是可以发现每过5秒,第一段程序会输出10个字符(包括换行),而第二段程序只会输出5个。而且我们经过分析不难发现
str[MAXLINE]
数组在水平触发时,每次是全部输出的,而边缘触发情况下,每次只输出了一半,这是因为我们父进程读的时候只读了一半。这就说明了,在水平触发模式下,只要有剩余的数据,
epoll_wait
会一直通知你,而在边缘触发模式下,则每个文件描述符只会通知一次。


那么问题来了,很明显传过来的数据我们是需要的,为了把数据读完,就需要重复调用
read
来读取,考虑这样一种情况,每次循环使用
read
读取10个字节,但是我们的数据总量只有21个字节,那么经过两次读取之后,还剩1个字节,再次读取时,由于不满足就会阻塞(是否阻塞要由设备的属性和设定所定,一般来说,读字符终端、网络的socket描述符、管道等会阻塞,而读磁盘上的文件一般不会)。阻塞在这肯定会影响程序的效率的。

解决方法是,当我们使用边缘触发时,将对应的文件描述符设置为非阻塞即可。

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define MAXLINE 10

int main()
{
pid_t pid;
int fd[2];
int i;
char str[MAXLINE], ch = 'a';
bzero(str, sizeof(str));
//使用管道,fd[0]默认是读端,fd[1]默认是写端
pipe(fd);
pid = fork();
if(pid == 0)  //child 负责写端
{
close(fd[0]);
while(1)
{
for(i = 0; i < MAXLINE / 2; i++)
{
str[i] = ch;
}
ch++;
str[i - 1] = '\n';
for(; i < MAXLINE; i++)
{
str[i] = ch;
}
str[i - 1] = '\n';

write(fd[1], str, sizeof(str));
sleep(5);
}

}
else if(pid > 0) //parent 负责读端
{
close(fd[1]);
//设置非阻塞
int flag = fcntl(fd[0], F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
struct epoll_event event;
struct epoll_event resevent[10];

int res;
//调用epoll_create创建红黑树树根
int efd = epoll_create(10);
event.data.fd = fd[0];
//触发方式默认是EPOLLLT
event.events = EPOLLIN;
//将读端文件描述符加入epoll监听的树中
epoll_ctl(efd, EPOLL_CTL_ADD, fd[0], &event);

int len;
while(1)
{
//当子进程发送数据时,就会触发事件进行读事件
res = epoll_wait(efd, resevent, 10, -1);
if(resevent[0].data.fd == fd[0])
{
//循环读取,直到读完为止
while((len = read(fd[0], str, MAXLINE/2)) > 0)
{
write(STDOUT_FILENO, str, len);
}
}
}
close(efd);
}
if(pid > 0)
close(fd[0]);
else
close(fd[1]);
return 0;
}


边缘触发比水平触发更高效的原因:不会让同一个文件描述符多次被处理,比如有些文件描述符已经不需要再读写了,但是在水平触发下每次都会返回,而边缘触发只会返回一次。

最后提醒一点,如果设置边缘触发,则必须将对应的文件描述符设置为非阻塞模式并且循环读取数据。否则会导致程序的效率大大下降。

poll和epoll默认采用的都是水平触发,只是epoll可以修改成边缘触发。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: