1 边沿触发与水平触发
这两个实际是由模拟电路上的高低电频来的,低电压(0)到高电压(1)为上升沿,高电压(1)到低电压(0)为下降沿,高电压(1)到高电压(1)为水平沿。水平沿对应水平触发,上升、下降沿对应边沿触发
-
边沿触发:epoll ET模式
-
水平触发:epoll LT模式
通俗的说,当客户端向服务端写数据,假如1000B的数据,服务端第一次读了500B后,还剩下500B在缓冲区中,假如此时能触发epoll_wait
函数的监听读事件,就叫水平触发,假如此时epoll_wait
函数不触发,那么就叫边沿触发。
代码演示
如下客户端与服务端之间的演示。客户端先写五个字节’aaaa\n’,再写’bbbb\n’五个字节共十个字节一次发给服务端,然后睡眠5秒;服务端将event.events
=属性设置EPOLLIN
时为水平触发,此时服务端每次读5个字节,读完5个字节后,剩下的5个字节会存在缓冲区,因为此时为水平触发,所以epoll_wait
函数仍然会触发读监听事件,会再次调用然后将这5个字节读出来,最后打印结果为:
aaaa
bbbb
cccc
dddd
...
此时服务端将将event.events
=属性设置改为EPOLLIN | EPOLLET
,将会开启边沿触发模式,此时服务端读走5个字符后,缓冲区内还剩5个字符,epoll_wait
函数也不会被调用去将这五个字符读出,只会等到5秒后客户端在发来数据时再调用,将缓冲区内的5个字符读出。此时的打印结果为:
aaaa
bbbb
cccc
...
服务端
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <ctype.h>
#include <sys/epoll.h>
#define IP "127.0.0.1"
#define PORT 8888
#define READNUM 10
int main()
{
int sfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof (serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr);
socklen_t serv_len = sizeof (serv_addr);
bind(sfd,(struct sockaddr*)&serv_addr,serv_len);
listen(sfd,128);
struct sockaddr_in clie_addr;
socklen_t clie_len = sizeof (clie_addr);
int cfd = accept(sfd,(struct sockaddr*)&clie_addr,&clie_len);
char acceptbuf[BUFSIZ];
printf("%s connected, port = %d\n",inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,acceptbuf,sizeof (acceptbuf))
,ntohs(clie_addr.sin_port));
int epfd = epoll_create(10);
struct epoll_event event;
event.events = EPOLLIN|EPOLLET;
event.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
while(1){
struct epoll_event outevent[10];
int nready = epoll_wait(epfd,outevent,sizeof (outevent),-1);
for(int i = 0; i < nready; i++){
if(!(outevent[i].events & EPOLLIN))
continue;
char buf[BUFSIZ];
memset(buf,0,sizeof (buf));
if(outevent[i].data.fd == cfd){
int len = read(cfd,buf,READNUM/2);
write(STDOUT_FILENO,buf,len);
}
}
}
close(sfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define PORT 8888
#define IP "127.0.0.1"
int main()
{
int sfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in clie_addr;
bzero(&clie_addr,sizeof (clie_addr));
clie_addr.sin_family = AF_INET;
clie_addr.sin_port = htons(PORT);
inet_pton(AF_INET,IP,&clie_addr.sin_addr.s_addr);
socklen_t clie_len = sizeof (clie_addr);
connect(sfd,(struct sockaddr*)&clie_addr,clie_len);
char buf[BUFSIZ];
memset(buf,0,sizeof (buf));
char ch = 'a';
while(1){
int i = 0;
for(; i < 5; i++){
buf[i] = ch;
}
buf[i-1] = '\n';
ch++;
for(;i<10;i++){
buf[i] = ch;
}
buf[i-1] = '\n';
ch++;
write(sfd,buf,10);
sleep(5);
}
return 0;
}
边沿触发与水平触发对比
情况一
当客户端与服务端通信时,假如每次要发5000B的数据,假设这5000B数据中前50B数据包含了剩下的4950B数据的信息
- 此情况下边沿触发好过水平触发,因为读50B就可以做出判定剩下的4950B数据对客户端有没有用,需不需要读,不需要就不读,清空缓冲区即可
- 水平触发即便知道剩下的数据不需要也要一直读完才行,会拖慢程序执行效率。
情况二
在使用readn
函数要读取500B数据时,假设此时服务端只发来了200B数据,readn
函数会发生阻塞,等待剩下300B数据的到来,此时如果采用边沿触发模式
readn
会在下一次数据到来前阻塞,而readn
函数阻塞等待,导致epoll_wait
函数无法监听下次数据的到来,readn
又在阻塞等下次数据到来,最后的结果就是形成“死锁”。
此时可以使用fcntl
函数设置readn
函数为不阻塞。这也就引出了第三种模型epoll非阻塞IO模型。
2 epoll非阻塞IO模型
代码实例
客户端同上,服务端有所修改
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <ctype.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define IP "127.0.0.1"
#define PORT 8888
#define READNUM 10
int main()
{
int sfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof (serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr);
socklen_t serv_len = sizeof (serv_addr);
bind(sfd,(struct sockaddr*)&serv_addr,serv_len);
listen(sfd,128);
int epfd = epoll_create(10);
struct epoll_event event;
event.events = EPOLLIN|EPOLLET;
struct sockaddr_in clie_addr;
socklen_t clie_len = sizeof (clie_addr);
int cfd = accept(sfd,(struct sockaddr*)&clie_addr,&clie_len);
char acceptbuf[BUFSIZ];
printf("%s connected, port = %d\n",inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,acceptbuf,sizeof (acceptbuf))
,ntohs(clie_addr.sin_port));
int flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
event.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
while(1){
struct epoll_event outevent[10];
int nready = epoll_wait(epfd,outevent,sizeof (outevent),-1);
for(int i = 0; i < nready; i++){
if(!(outevent[i].events & EPOLLIN))
continue;
char buf[BUFSIZ];
memset(buf,0,sizeof (buf));
int len = 0;
if(outevent[i].data.fd == cfd){
while((len = read(cfd,buf,READNUM/2)) > 0)
write(STDOUT_FILENO,buf,len);
}
}
}
close(sfd);
return 0;
}
运行结果:
aaaa
bbbb
cccc
dddd
...
采用边沿触发模式,通过fcntl
函数修改文件属性为非阻塞,相当于使用边沿触发实现了水平触发的效果。这种方法相比单纯的水平触发与边沿触发模式都要好。相比较于单纯的水平触发,调用epoll_wait
函数的次数少了,增加了代码的执行效率。相比较于单纯的边沿触发,增加了灵活性,且有效避免了“死锁”的出现。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)