epoll的两种触发模式

72 篇文章 0 订阅
 

epoll的两种触发模式

  8692人阅读  评论(1)  收藏  举报
  分类:
 
linux异步IO浅析 
http://hi.baidu.com/_kouu/blog/item/e225f67b337841f42f73b341.html 

epoll有两种模式,Edge Triggered(简称ET) 和 Level Triggered(简称LT).在采用这两种模式时要注意的是,如果采用ET模式,那么仅当状态发生变化时才会通知,而采用LT模式类似于原来的select/poll操作,只要还有没有处理的事件就会一直通知. 

以代码来说明问题: 
首先给出server的代码,需要说明的是每次accept的连接,加入可读集的时候采用的都是ET模式,而且接收缓冲区是5字节的,也就是每次只接收5字节的数据: 
Java代码   收藏代码
  1. #include <iostream>  
  2. #include <sys/socket.h>  
  3. #include <sys/epoll.h>  
  4. #include <netinet/in.h>  
  5. #include <arpa/inet.h>  
  6. #include <fcntl.h>  
  7. #include <unistd.h>  
  8. #include <stdio.h>  
  9. #include <errno.h>  
  10.   
  11. using namespace std;  
  12.   
  13. #define MAXLINE 5  
  14. #define OPEN_MAX 100  
  15. #define LISTENQ 20  
  16. #define SERV_PORT 5000  
  17. #define INFTIM 1000  
  18.   
  19. void setnonblocking(int sock)  
  20. {  
  21.     int opts;  
  22.     opts=fcntl(sock,F_GETFL);  
  23.     if(opts<0)  
  24.     {  
  25.         perror("fcntl(sock,GETFL)");  
  26.         exit(1);  
  27.     }  
  28.     opts = opts|O_NONBLOCK;  
  29.     if(fcntl(sock,F_SETFL,opts)<0)  
  30.     {  
  31.         perror("fcntl(sock,SETFL,opts)");  
  32.         exit(1);  
  33.     }     
  34. }  
  35.   
  36. int main()  
  37. {  
  38.     int i, maxi, listenfd, connfd, sockfd,epfd,nfds;  
  39.     ssize_t n;  
  40.     char line[MAXLINE];  
  41.     socklen_t clilen;  
  42.     //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件  
  43.     struct epoll_event ev,events[20];  
  44.     //生成用于处理accept的epoll专用的文件描述符  
  45.     epfd=epoll_create(256);  
  46.     struct sockaddr_in clientaddr;  
  47.     struct sockaddr_in serveraddr;  
  48.     listenfd = socket(AF_INET, SOCK_STREAM, 0);  
  49.     //把socket设置为非阻塞方式  
  50.     //setnonblocking(listenfd);  
  51.     //设置与要处理的事件相关的文件描述符  
  52.     ev.data.fd=listenfd;  
  53.     //设置要处理的事件类型  
  54.     ev.events=EPOLLIN|EPOLLET;  
  55.     //ev.events=EPOLLIN;  
  56.     //注册epoll事件  
  57.     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);  
  58.     bzero(&serveraddr, sizeof(serveraddr));  
  59.     serveraddr.sin_family = AF_INET;  
  60.     char *local_addr="127.0.0.1";  
  61.     inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);  
  62.     serveraddr.sin_port=htons(SERV_PORT);  
  63.     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));  
  64.     listen(listenfd, LISTENQ);  
  65.     maxi = 0;  
  66.     for ( ; ; ) {  
  67.         //等待epoll事件的发生  
  68.         nfds=epoll_wait(epfd,events,20,500);  
  69.         //处理所发生的所有事件       
  70.         for(i=0;i<nfds;++i)  
  71.         {  
  72.             if(events[i].data.fd==listenfd)  
  73.             {  
  74.                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);  
  75.                 if(connfd<0){  
  76.                     perror("connfd<0");  
  77.                     exit(1);  
  78.                 }  
  79.                 //setnonblocking(connfd);  
  80.                 char *str = inet_ntoa(clientaddr.sin_addr);  
  81.                 cout << "accapt a connection from " << str << endl;  
  82.                 //设置用于读操作的文件描述符  
  83.                 ev.data.fd=connfd;  
  84.                 //设置用于注测的读操作事件  
  85.                 ev.events=EPOLLIN|EPOLLET;  
  86.                 //ev.events=EPOLLIN;  
  87.                 //注册ev  
  88.                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);  
  89.             }  
  90.             else if(events[i].events&EPOLLIN)  
  91.             {  
  92.                 cout << "EPOLLIN" << endl;  
  93.                 if ( (sockfd = events[i].data.fd) < 0)   
  94.                     continue;  
  95.                 if ( (n = read(sockfd, line, MAXLINE)) < 0) {  
  96.                     if (errno == ECONNRESET) {  
  97.                         close(sockfd);  
  98.                         events[i].data.fd = -1;  
  99.                     } else  
  100.                         std::cout<<"readline error"<<std::endl;  
  101.                 } else if (n == 0) {  
  102.                     close(sockfd);  
  103.                     events[i].data.fd = -1;  
  104.                 }  
  105.                 line[n] = '\0';  
  106.                 cout << "read " << line << endl;  
  107.                 //设置用于写操作的文件描述符  
  108.                 ev.data.fd=sockfd;  
  109.                 //设置用于注测的写操作事件  
  110.                 ev.events=EPOLLOUT|EPOLLET;  
  111.                 //修改sockfd上要处理的事件为EPOLLOUT  
  112.                 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  
  113.             }  
  114.             else if(events[i].events&EPOLLOUT)  
  115.             {     
  116.                 sockfd = events[i].data.fd;  
  117.                 write(sockfd, line, n);  
  118.                 //设置用于读操作的文件描述符  
  119.                 ev.data.fd=sockfd;  
  120.                 //设置用于注测的读操作事件  
  121.                 ev.events=EPOLLIN|EPOLLET;  
  122.                 //修改sockfd上要处理的事件为EPOLIN  
  123.                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  
  124.             }  
  125.         }  
  126.     }  
  127.     return 0;  
  128. }  


下面给出测试所用的Perl写的client端,在client中发送10字节的数据,同时让client在发送完数据之后进入死循环, 也就是在发送完之后连接的状态不发生改变--既不再发送数据, 也不关闭连接,这样才能观察出server的状态: 
Java代码   收藏代码
  1. #!/usr/bin/perl  
  2.   
  3. use IO::Socket;  
  4.   
  5. my $host = "127.0.0.1";  
  6. my $port = 5000;  
  7.   
  8. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";  
  9. my $msg_out = "1234567890";  
  10. print $socket $msg_out;  
  11. print "now send over, go to sleep \n";  
  12.   
  13. while (1)  
  14. {  
  15.     sleep(1);  
  16. }  

运行server和client发现,server仅仅读取了5字节的数据,而client其实发送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事件了. 
(友情提示:上面的这个测试客户端,当你关闭它的时候会再次出发IO可读事件给server,此时server就会去读取剩下的5字节数据了,但是这一事件与前面描述的ET性质并不矛盾.) 

如果我们把client改为这样: 
Java代码   收藏代码
  1. #!/usr/bin/perl  
  2.   
  3. use IO::Socket;  
  4.   
  5. my $host = "127.0.0.1";  
  6. my $port = 5000;  
  7.   
  8. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";  
  9. my $msg_out = "1234567890";  
  10. print $socket $msg_out;  
  11. print "now send over, go to sleep \n";  
  12. sleep(5);  
  13. print "5 second gone send another line\n";  
  14. print $socket $msg_out;  
  15.   
  16. while (1)  
  17. {  
  18.     sleep(1);  
  19. }  


可以发现,在server接收完5字节的数据之后一直监听不到client的事件,而当client休眠5秒之后重新发送数据,server再次监听到了变化,只不过因为只是读取了5个字节,仍然有10个字节的数据(client第二次发送的数据)没有接收完. 

如果上面的实验中,对accept的socket都采用的是LT模式,那么只要还有数据留在buffer中,server就会继续得到通知,读者可以自行改动代码进行实验. 

基于这两个实验,可以得出这样的结论:ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的. 
补充说明一下这里一直强调的"状态变化"是什么: 

1)对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化.但是,如果在一个时间同时接收了N个连接(N>1),但是监听socket只accept了一个连接,那么其它未 accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于一般的socket,就如例子中而言,如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化. 

2)对于监听可写事件时,同理可推,不再详述. 

而不论是监听可读还是可写,对方关闭socket连接都将造成状态发生变化,比如在例子中,如果强行中断client脚本,也就是主动中断了socket连接,那么都将造成server端发生状态的变化,从而server得到通知,将已经在本方缓冲区中的数据读出. 

把前面的描述可以总结如下:仅当对方的动作(发出数据,关闭连接等)造成的事件才能导致状态发生变化,而本方协议栈中已经处理的事件(包括接收了对方的数据,接收了对方的主动连接请求)并不是造成状态发生变化的必要条件,状态变化一定是对方造成的.所以在ET模式下的,必须一直处理到出错或者完全处理完毕,才能进行下一个动作,否则可能会发生错误. 

另外,从这个例子中,也可以阐述一些基本的网络编程概念.首先,连接的两端中,一端发送成功并不代表着对方上层应用程序接收成功, 就拿上面的client测试程序来说,10字节的数据已经发送成功,但是上层的server并没有调用read读取数据,因此发送成功仅仅说明了数据被对方的协议栈接收存放在了相应的buffer中,而上层的应用程序是否接收了这部分数据不得而知;同样的,读取数据时也只代表着本方协议栈的对应buffer中有数据可读,而此时时候在对端是否在发送数据也不得而知. 




epoll精髓 
在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。 
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明: 
#define __FD_SETSIZE    1024 
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。 

epoll的接口非常简单,一共就三个函数: 
1. int epoll_create(int size); 
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。 


2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示: 
EPOLL_CTL_ADD:注册新的fd到epfd中; 
EPOLL_CTL_MOD:修改已经注册的fd的监听事件; 
EPOLL_CTL_DEL:从epfd中删除一个fd; 
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下: 
Java代码   收藏代码
  1. struct epoll_event {  
  2.   __uint32_t events;  /* Epoll events */  
  3.   epoll_data_t data;  /* User data variable */  
  4. };  


events可以是以下几个宏的集合: 
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 


3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。 

-------------------------------------------------------------------------------------------- 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值