Chinaunix

标题: epoll学习笔记 [打印本页]

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

以代码来说明问题:
首先给出server的代码,需要说明的是每次accept的连接,加入可读集的时候采用的都是ET模式,而且接收缓冲区是5字节的,也就是每次只接收5字节的数据:


  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. using namespace std;

  11. #define MAXLINE 5
  12. #define OPEN_MAX 100
  13. #define LISTENQ 20
  14. #define SERV_PORT 5000
  15. #define INFTIM 1000

  16. void setnonblocking(int sock)
  17. {
  18.     int opts;
  19.     opts=fcntl(sock,F_GETFL);
  20.     if(opts<0)
  21.     {
  22.         perror("fcntl(sock,GETFL)");
  23.         exit(1);
  24.     }
  25.     opts = opts|O_NONBLOCK;
  26.     if(fcntl(sock,F_SETFL,opts)<0)
  27.     {
  28.         perror("fcntl(sock,SETFL,opts)");
  29.         exit(1);
  30.     }   
  31. }

  32. int main()
  33. {
  34.     int i, maxi, listenfd, connfd, sockfd,epfd,nfds;
  35.     ssize_t n;
  36.     char line[MAXLINE];
  37.     socklen_t clilen;
  38.     //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
  39.     struct epoll_event ev,events[20];
  40.     //生成用于处理accept的epoll专用的文件描述符
  41.     epfd=epoll_create(256);
  42.     struct sockaddr_in clientaddr;
  43.     struct sockaddr_in serveraddr;
  44.     listenfd = socket(AF_INET, SOCK_STREAM, 0);
  45.     //把socket设置为非阻塞方式
  46.     //setnonblocking(listenfd);
  47.     //设置与要处理的事件相关的文件描述符
  48.     ev.data.fd=listenfd;
  49.     //设置要处理的事件类型
  50.     ev.events=EPOLLIN|EPOLLET;
  51.     //ev.events=EPOLLIN;
  52.     //注册epoll事件
  53.     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
  54.     bzero(&serveraddr, sizeof(serveraddr));
  55.     serveraddr.sin_family = AF_INET;
  56.     char *local_addr="127.0.0.1";
  57.     inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);
  58.     serveraddr.sin_port=htons(SERV_PORT);
  59.     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
  60.     listen(listenfd, LISTENQ);
  61.     maxi = 0;
  62.     for ( ; ; ) {
  63.         //等待epoll事件的发生
  64.         nfds=epoll_wait(epfd,events,20,500);
  65.         //处理所发生的所有事件     
  66.         for(i=0;i<nfds;++i)
  67.         {
  68.             if(events[i].data.fd==listenfd)
  69.             {
  70.                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
  71.                 if(connfd<0){
  72.                     perror("connfd<0");
  73.                     exit(1);
  74.                 }
  75.                 //setnonblocking(connfd);
  76.                 char *str = inet_ntoa(clientaddr.sin_addr);
  77.                 cout << "accapt a connection from " << str << endl;
  78.                 //设置用于读操作的文件描述符
  79.                 ev.data.fd=connfd;
  80.                 //设置用于注测的读操作事件
  81.                 ev.events=EPOLLIN|EPOLLET;
  82.                 //ev.events=EPOLLIN;
  83.                 //注册ev
  84.                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
  85.             }
  86.             else if(events[i].events&EPOLLIN)
  87.             {
  88.                 cout << "EPOLLIN" << endl;
  89.                 if ( (sockfd = events[i].data.fd) < 0)
  90.                     continue;
  91.                 if ( (n = read(sockfd, line, MAXLINE)) < 0) {
  92.                     if (errno == ECONNRESET) {
  93.                         close(sockfd);
  94.                         events[i].data.fd = -1;
  95.                     } else
  96.                         std::cout<<"readline error"<<std::endl;
  97.                 } else if (n == 0) {
  98.                     close(sockfd);
  99.                     events[i].data.fd = -1;
  100.                 }
  101.                 line[n] = '\0';
  102.                 cout << "read " << line << endl;
  103.                 //设置用于写操作的文件描述符
  104.                 ev.data.fd=sockfd;
  105.                 //设置用于注测的写操作事件
  106.                 ev.events=EPOLLOUT|EPOLLET;
  107.                 //修改sockfd上要处理的事件为EPOLLOUT
  108.                 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
  109.             }
  110.             else if(events[i].events&EPOLLOUT)
  111.             {   
  112.                 sockfd = events[i].data.fd;
  113.                 write(sockfd, line, n);
  114.                 //设置用于读操作的文件描述符
  115.                 ev.data.fd=sockfd;
  116.                 //设置用于注测的读操作事件
  117.                 ev.events=EPOLLIN|EPOLLET;
  118.                 //修改sockfd上要处理的事件为EPOLIN
  119.                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
  120.             }
  121.         }
  122.     }
  123.     return 0;
  124. }
复制代码


下面给出测试所用的Perl写的client端,在client中发送10字节的数据,同时让client在发送完数据之后进入死循环, 也就是在发送完之后连接的状态不发生改变--既不再发送数据, 也不关闭连接,这样才能观察出server的状态:


  1. #!/usr/bin/perl

  2. use IO::Socket;

  3. my $host = "127.0.0.1";
  4. my $port = 5000;

  5. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
  6. my $msg_out = "1234567890";
  7. print $socket $msg_out;
  8. print "now send over, go to sleep\n";

  9. while (1)
  10. {
  11.     sleep(1);
  12. }
复制代码


运行server和client发现,server仅仅读取了5字节的数据,而client其实发送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事件了.

如果我们把client改为这样:


  1. #!/usr/bin/perl

  2. use IO::Socket;

  3. my $host = "127.0.0.1";
  4. my $port = 5000;

  5. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
  6. my $msg_out = "1234567890";
  7. print $socket $msg_out;
  8. print "now send over, go to sleep\n";
  9. sleep(5);
  10. print "5 second gonesend another line\n";
  11. print $socket $msg_out;

  12. while (1)
  13. {
  14.     sleep(1);
  15. }

复制代码


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

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

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

另外,从这个例子中,也可以阐述一些基本的网络编程概念.首先,连接的两端中,一端发送成功并不代表着对方上层应用程序接收成功, 就拿上面的client测试程序来说,10字节的数据已经发送成功,但是上层的server并没有调用read读取数据,因此发送成功仅仅说明了数据被对方的协议栈接收存放在了相应的buffer中,而上层的应用程序是否接收了这部分数据不得而知;同样的,读取数据时也只代表着本方协议栈的对应 buffer中有数据可读,而此时时候在对端是否在发送数据也不得而知.
作者: cugb_cat    时间: 2008-04-29 20:15
在使用非阻塞模式时,ET模式下要一直循环直到EAGAIN错误。
还有楼主说的那个相应的buffer,就是内核缓冲区,这个缓冲区与文件IO中文件系统中的缓冲区概念是差不多的。
作者: flw    时间: 2008-04-29 21:31
写的很不错。
作者: swordfish.cn    时间: 2008-04-29 22:51
ET 和 LT,乍看乍像电子里面的东西。
作者: cugb_cat    时间: 2008-04-29 22:53
原帖由 swordfish.cn 于 2008-4-29 22:51 发表
ET 和 LT,乍看乍像电子里面的东西。

就是那个意思,边沿触发和水平触发
作者: swordfish.cn    时间: 2008-04-29 22:58
原帖由 cugb_cat 于 2008-4-29 22:53 发表

就是那个意思,边沿触发和水平触发


是仅仅取用电子里面的概念还是真的检测硬件的状态呢?
作者: cugb_cat    时间: 2008-04-29 23:02
原帖由 swordfish.cn 于 2008-4-29 22:58 发表


是仅仅取用电子里面的概念还是真的检测硬件的状态呢?

我觉得应该是概念类似。
作者: 涩兔子    时间: 2008-04-30 09:25
注释用英文,然后版本控制一下就更好了
作者: cookis    时间: 2008-04-30 12:04
这不是网上流传最广的那个epoll入门实例吗.
亲历亲为.. 8 错..
作者: converse    时间: 2008-04-30 12:21
补充说明一下这里一直强调的"状态变化"是什么:

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

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

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

把前面的描述可以总结如下:仅当对方的动作(发出数据,关闭连接等)造成的事件才能导致状态发生变化,而本方协议栈中已经处理的事件(包括接收了对方的数据,接收了对方的主动连接请求)并不是造成状态发生变化的必要条件,状态变化一定是对方造成的.所以在ET模式下的,必须一直处理到出错或者完全处理完毕,才能进行下一个动作,否则可能会发生错误.
作者: yulc    时间: 2008-05-15 11:51
原帖由 cugb_cat 于 2008-4-29 20:15 发表
在使用非阻塞模式时,ET模式下要一直循环直到EAGAIN错误。
还有楼主说的那个相应的buffer,就是内核缓冲区,这个缓冲区与文件IO中文件系统中的缓冲区概念是差不多的。



前一句话我的理解与你不同,我的理解是:
在ET模式下,不需要循环读到EAGAIN.只要你read返回的数据小于你请求的数据时,则认为已读尽缓冲区了.

请看这个:

       Q9     Do I need to continuously read/write an fd until EAGAIN when using the EPOLLET  flag  (  Edge  Triggered
              behaviour ) ?

       A9     No  you  don't. Receiving an event from epoll_wait(2) should suggest to you that such file descriptor is
              ready for the requested I/O operation. You have simply to consider it ready until you will  receive  the
              next  EAGAIN.  When and how you will use such file descriptor is entirely up to you. Also, the condition
              that the read/write I/O space is exhausted can be detected by checking the  amount  of  data  read/write
              from/to  the target file descriptor. For example, if you call read(2) by asking to read a certain amount
              of data and read(2) returns a lower number of bytes, you can be sure to  have  exhausted  the  read  I/O
              space for such file descriptor. Same is valid when writing using the write(2) function.
作者: nicsky    时间: 2008-05-16 14:17

作者: cookis    时间: 2008-05-16 21:20
标题: 回复 #11 yulc 的帖子
的确
作者: coneagoe    时间: 2008-05-22 14:04
楼主幸苦了,支持楼主
作者: cugb_cat    时间: 2008-05-22 14:11
原帖由 yulc 于 2008-5-15 11:51 发表



前一句话我的理解与你不同,我的理解是:
在ET模式下,不需要循环读到EAGAIN.只要你read返回的数据小于你请求的数据时,则认为已读尽缓冲区了.

请看这个:

       Q9     Do I need to continuously re ...

UDP中是,TCP就不一定了吧?可能内核只收到了一部分,剩下的还没到达,这时read就可能返回小于请求的数据长度。
作者: cookis    时间: 2008-05-22 15:00
标题: 回复 #15 cugb_cat 的帖子
个人认为epoll 提到的状态改变只是针对I/O缓冲区的状态改变. 跟内核接收没什么关系吧.

这个应该是一个生产者与消费者的关系. 内核只管接收..收到后放到缓冲区. epoll 或 select 会检查
缓冲区是否有数据. 有就触发可读事件.


再有: recv 返回值小于请求的长度. 说明缓冲区已经没有可读数据. 当你再读肯定会触发EAGAIN.
当然你可以再读一次.判断 errno ==  EAGAIN 然后再退出.. 我觉得这两个判断条件最终都是归
结到EAGAIN..依据应该都是一样的.

我想应该是这样的.

[ 本帖最后由 cookis 于 2008-5-22 15:06 编辑 ]
作者: cugb_cat    时间: 2008-05-22 15:03
原帖由 cookis 于 2008-5-22 15:00 发表
个人认为epoll 提到的状态改变只是针对I/O缓冲区的状态改变. 跟内核接收没什么关系吧.

这个应该是一个生产者与消费者的关系. 内核只管接收..收到后放到缓冲区. epoll 或 select 会检查
缓冲区是否有数据. 有 ...

是啊,但是当缓冲区中的数据不够read的请求数据长度呢?
作者: cookis    时间: 2008-05-22 15:06
你回得好快..
作者: cugb_cat    时间: 2008-05-22 15:38
原帖由 cookis 于 2008-5-22 15:06 发表
你回得好快..

我觉得这样还是不保险的。
作者: marxn    时间: 2008-05-22 21:42
原帖由 cookis 于 2008-5-22 15:00 发表
个人认为epoll 提到的状态改变只是针对I/O缓冲区的状态改变. 跟内核接收没什么关系吧.

这个应该是一个生产者与消费者的关系. 内核只管接收..收到后放到缓冲区. epoll 或 select 会检查
缓冲区是否有数据. 有 ...


recv 返回值小于请求的长度时说明缓冲区已经没有可读数据. 但再读不一定会触发EAGAIN.有可能返回0表示TCP连接已被关闭。
作者: UnixStudier    时间: 2008-05-23 09:40
converse:"
1)对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化.但是,如果在一个时间同时接收了N个连接(N>1),但是监听socket只accept了一个连接,那么其它未 accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于一般的socket,就如例子中而言,如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化."
这个确实是个问题。server端的代码就有这里说的问题。listenfd不应该用epoll监听。

[ 本帖最后由 UnixStudier 于 2008-5-23 09:41 编辑 ]
作者: cookis    时间: 2008-05-23 09:40
标题: 回复 #20 marxn 的帖子
有创意...

NOTE: EAGAIN 是返回 -1 的errno..
作者: UnixStudier    时间: 2008-05-23 09:43
关于recv的缓冲区大小,
每次recv的时候我用的缓冲区大小都是tcp缓冲区的大小,这样只读一次,不用循环读到EGAIN。暂时还发现什么问题。
作者: yangsf5    时间: 2008-05-23 10:13
“你们都是读到EGAIN就不读了?

我一开始也是,但是读不完。。

现在改的是读到EGAIN,继续读,直到出错或者读完。。貌似目前还正常。。”


补充:以上是原帖。当时概念不是很清晰。请勿借鉴。:wink:
                         --- 09.03.12

[ 本帖最后由 yangsf5 于 2009-3-12 09:25 编辑 ]
作者: cookis    时间: 2008-05-23 11:06
标题: 回复 #24 yangsf5 的帖子
读到EAGAIN 还会出什么错?
作者: cugb_cat    时间: 2008-05-23 11:12
原帖由 yangsf5 于 2008-5-23 10:13 发表
你们都是读到EGAIN就不读了?

我一开始也是,但是读不完。。

现在改的是读到EGAIN,继续读,直到出错或者读完。。貌似目前还正常。。

EAGAIN就是错误啊。
作者: marxn    时间: 2008-05-23 14:11
原帖由 cookis 于 2008-5-23 09:40 发表
有创意...

NOTE: EAGAIN 是返回 -1 的errno..


是啊,我说的哪里错了?
我的意思是在recv读取到的字节数小于请求的字节数时,下次读不一定会返回-1。有什么问题吗?
作者: marxn    时间: 2008-05-23 14:25
原帖由 yangsf5 于 2008-5-23 10:13 发表
你们都是读到EGAIN就不读了?

我一开始也是,但是读不完。。

现在改的是读到EGAIN,继续读,直到出错或者读完。。貌似目前还正常。。


如果你的客户端是一个低速网络设备,你会发现服务器的CPU占用率瞬间上升至100%
作者: zl_linux    时间: 2008-06-03 13:20
标题: epoll ET模式问题
把文件描述符设置为ET模式后,能否保证有新的数据到来后,内核只通知一次?
作者: zl_linux    时间: 2008-06-03 13:26
标题: epoll ET模式问题
我把文件描述符设置为ET后,在相应的sockfd上读取数据完毕后,为什么又一次从epoll_wait中返回了刚才处理过的文件描述符,但没有数据可读
????
作者: zl_linux    时间: 2008-06-03 15:09
标题: 对十楼的质疑
“如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化.”
我测了一下,上述情况epoll_wait下次仍会返回该sockfd的
作者: cookis    时间: 2008-06-03 18:35
我也发现有问题..我在接收的地方sleep(10).结果epoll_wait 马上就又检测到可读事件. 倒致多个线程全都是去接收了..
作者: vbs100    时间: 2008-12-02 19:38
原帖由 marxn 于 2008-5-22 21:42 发表


recv 返回值小于请求的长度时说明缓冲区已经没有可读数据. 但再读不一定会触发EAGAIN.有可能返回0表示TCP连接已被关闭。


这个没有必要的,没有数据就不用再读了,tcp断开的时候会触发EPOLLHUP事件

原帖由 UnixStudier 于 2008-5-23 09:40 发表
converse:"
1)对于监听可读事件时,如果是socket是监听socket,那么当有新的主动连接到来为状态发生变化;对一般的socket而言,协议栈中相应的缓冲区有新的数据为状态发生变化.但是,如果在一个时间同时接收了N个连 ...


在ET模式下 accetp应该一直运行到EAGAIN错误 这样就可以了
作者: xinglp    时间: 2008-12-02 22:09
原帖由 vbs100 于 2008-12-2 19:38 发表
这个没有必要的,没有数据就不用再读了,tcp断开的时候会触发EPOLLHUP事件
在ET模式下 accetp应该一直运行到EAGAIN错误 这样就可以了


TCP断开大部分时候触发的是EPOLLIN吧,recv得0
作者: vbs100    时间: 2008-12-04 14:46
Q9     Do I need to continuously read/write a file descriptor until EAGAIN when using the EPOLLET flag (edge-trig-
              gered behavior) ?

       A9     No  you  don't.   Receiving  an event from epoll_wait(2) should suggest to you that such file descriptor is
              ready for the requested I/O operation.  You have simply to consider it ready until  you  will  receive  the
              next  EAGAIN.   When  and how you will use such file descriptor is entirely up to you.  Also, the condition
              that the read/write I/O space is exhausted can be detected by checking the amount of data read from / writ-
              ten  to the target file descriptor.  For example, if you call read(2) by asking to read a certain amount of
              data and read(2) returns a lower number of bytes, you can be sure of having exhausted the  read  I/O  space
              for such file descriptor.  The same is true when writing using write(2).




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2