epoll的默认模式是水平触发。
先大概了解一下这两种触发模式有什么不同:
水平触发(Level Trigger,也称条件触发):只要满足条件,就触发一个事件(只要有数据还未读完,就会一直触发)
边缘触发(Edge Trigger):每当状态发生变化时就触发一个事件。
可能概念不容易理解,这里举一个例子大概就能明白两者的区别了:比如某个人让你去买几袋酱油,你只买了一袋回去,水平触发的做法就是他让你继续去把剩下的几袋酱油买回来,如果没有完成任务,就一直通知你;边缘触发的做法就是不管完没完成任务,反正他让你买了,买没买完就是你自己的事了,下次买酱油这件事他就不管了,会让你去做其它的事。
通过上面的例子,我们对边缘触发和水平触发有了一个大概的了解,下面通过代码来深入了解:
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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可以修改成边缘触发。