epoll深度分析

来源:互联网 发布:lol网络波动也被裁决 编辑:程序博客网 时间:2024/05/18 03:06
在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结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

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表示已超时。

--------------------------------------------------------------------------------------------

从man手册中,得到ET和LT的具体描述如下

EPOLL事件有两种模型:
Edge Triggered (ET)
Level Triggered (LT)

假如有这样一个例子:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......

Edge Triggered 工作模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
   i    基于非阻塞文件句柄
   ii   只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

Level Triggered 工作模式
相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。


然后详细解释ET, LT:

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。

在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)



另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,
读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取
while(rs)
{
  buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
  if(buflen < 0)
  {
    // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
    // 在这里就当作是该次事件已处理处.
    if(errno == EAGAIN)
     break;
    else
     return;
   }
   else if(buflen == 0)
   {
     // 这里表示对端的socket已正常关闭.
   }
   if(buflen == sizeof(buf)
     rs = 1;   // 需要再次读取
   else
     rs = 0;
}


还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.

ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
  ssize_t tmp;
  size_t total = buflen;
  const char *p = buffer;

  while(1)
  {
    tmp = send(sockfd, p, total, 0);
    if(tmp < 0)
    {
      // 当send收到信号时,可以继续写,但这里返回-1.
      if(errno == EINTR)
        return -1;

      // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,
      // 在这里做延时后再重试.
      if(errno == EAGAIN)
      {
        usleep(1000);
        continue;
      }

      return -1;
    }

    if((size_t)tmp == total)
      return buflen;

    total -= tmp;
    p += tmp;
  }

  return tmp;

}

以上转自http://www.cnblogs.com/OnlyXP/archive/2007/08/10/851222.html

===============================================


(1)导言:

首 先,我强烈建议大家阅读Richard Stevens著作《TCP/IP Illustracted Volume 1,2,3》和《UNIX Network Programming Volume 1,2》。虽然他离开我们大家已经5年多了,但是他的书依然是进入网络编程的最直接的道路。其中的3卷的《TCP/IP Illustracted》卷1是必读-如果你不了解tcp协议各个选项的详细定义,你就失去了优化程序重要的一个手段。卷2,3可以选读一下。比如卷2 讲解的是4.4BSD内核TCP/IP协议栈实现----这个版本的协议栈几乎影响了现在所有的主流os,但是因为年代久远,内容不一定那么vogue. 在这里我多推荐一本《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,以2.4内核讲解Linux TCP/IP实现,相当不错.作为一个现实世界中的实现,很多时候你必须作很多权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内 核中sk_buff结构为了追求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多大,sk_buff最小也有272的字节.

其 实对于socket应用层程序来说,《UNIX Network Programming Volume 1》意义更大一点.2003年的时候,这本书出了最新的第3版本,不过主要还是修订第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens给出了网络IO的基本模型。在这里最重要的莫过于select模型和Asynchronous I/O模型.从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方 案。最著名的windows完成端口实现的AIO,实际上也是内部用线程池实现的罢了,最后的结果是IO有个线程池,你应用也需要一个线程池...... 很多文档其实已经指出了这带来的线程contexttch带来的代价。

在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI那个。但是一直到2.6内核发布,网络 模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多 了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当帮助)。

所以,剩下的select模型基本上 就是我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺 点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案

(2)epoll的优点

<1>支持一个进程打开大数目的socket描述符(FD)

select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。

<2>IO效率不随FD数目增加而线性下降

传 统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但 是 select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降但是epoll不存在这个问题,它只会对"活跃"的socket进行操 作 ---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

<3>使用mmap加速内核与用户空间的消息传递。

这 点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很 重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。

<4>内核微调

这一点其实不算epoll的优点了,而是整个linux平台的优点。也许 你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可 以在运行时期动态调整这个内存pool(skb_head_pool)的大小--- 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。

(3)epoll的使用

令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式:LT和ET。

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,
在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了

(4)Leader/follower模式线程pool实现,以及和epoll的配合

.....未完成,主要是要避免过多的epoll_ctl调用,以及尝试使用EPOLLONESHOT加速......

(5)benchmark

.......未完成

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
一个简单的基于epoll的web server,性能还不错


我根据一个epoll的模型改了一个http server出来。只有129行,还可以精简不少,呵呵。
小测了一下,一秒钟处理了一万了请求。当然这里只是把现成的东西输出。没考虑到发送数据处理。和请求的解析。
注意了,epoll只基于linux 2.6内核的。其他平台不能用。





/*-------------------------------------------------------------------------------------------------
gcc -o httpd httpd.c -lpthread
author: wyezl
2006.4.28
---------------------------------------------------------------------------------------------------*/

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>

#define PORT 8888
#define MAXFDS 5000
#define EVENTSIZE 100

#define BUFFER "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\nContent-Type: text/html\r\n\r\nHello"

int epfd;
void *serv_epoll(void *p);
void setnonblocking(int fd)
{
int opts;
opts=fcntl(fd, F_GETFL);
if (opts < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
opts = opts | O_NONBLOCK;
if(fcntl(fd, F_SETFL, opts) < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
return;
}

int main(int argc, char *argv[])
{
int fd, cfd,opt=1;
struct epoll_event ev;
struct sockaddr_in sin, cin;
socklen_t sin_len = sizeof(struct sockaddr_in);
pthread_t tid;
pthread_attr_t attr;

epfd = epoll_create(MAXFDS);
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) <= 0)
{
fprintf(stderr, "socket failed\n");
return -1;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt));

memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_port = htons((short)(PORT));
sin.sin_addr.s_addr = INADDR_ANY;
if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) != 0)
{
fprintf(stderr, "bind failed\n");
return -1;
}
if (listen(fd, 32) != 0)
{
fprintf(stderr, "listen failed\n");
return -1;
}

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, serv_epoll, NULL) != 0)
{
fprintf(stderr, "pthread_create failed\n");
return -1;
}

while ((cfd = accept(fd, (struct sockaddr *)&cin, &sin_len)) > 0)
{
setnonblocking(cfd);
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
//printf("connect from %s\n",inet_ntoa(cin.sin_addr));
//printf("cfd=%d\n",cfd);
}

if (fd > 0)
close(fd);
return 0;
}

void *serv_epoll(void *p)
{
int i, ret, cfd, nfds;;
struct epoll_event ev,events[EVENTSIZE];
char buffer[512];

while (1)
{
nfds = epoll_wait(epfd, events, EVENTSIZE , -1);
//printf("nfds ........... %d\n",nfds);
for (i=0; i<nfds; i++)
{
if(events[i].events & EPOLLIN)
{
cfd = events[i].data.fd;
ret = recv(cfd, buffer, sizeof(buffer),0);
//printf("read ret..........= %d\n",ret);

ev.data.fd = cfd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, cfd, &ev);
}
else if(events[i].events & EPOLLOUT)
{
cfd = events[i].data.fd;
ret = send(cfd, BUFFER, strlen(BUFFER), 0);
//printf("send ret...........= %d\n", ret);

ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, &ev);
//shutdown(cfd, 1);
close(cfd);

}
}
}
return NULL;
}




下面是测试结果:

[yangjian2@localhost bin]$ ./ab -c 50 -n 10000 [url]http://202.108.xxx.xxx:8888/[/url]
This is ApacheBench, Version 2.0.41-dev <$Revision: 1.121.2.12 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, [url]http://www.zeustech.net/[/url]
Copyright (c) 1998-2002 The Apache Software Foundation, [url]http://www.apache.org/[/url]

Benchmarking 202.108.xxx.xxx (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Finished 10000 requests


Server Software: 
Server Hostname: 202.108.xxx.xxx
Server Port: 8888

Document Path: /
Document Length: 5 bytes

Concurrency Level: 50
Time taken for tests: 0.921732 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 872088 bytes
HTML transferred: 50120 bytes
Requests per second: 10849.14 [#/sec] (mean)
Time per request: 4.609 [ms] (mean)
Time per request: 0.092 [ms] (mean, across all concurrent requests)
Transfer rate: 923.26 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 1 1 0.9 2 3
Processing: 1 2 0.5 2 4
Waiting: 0 1 0.3 1 3
Total: 4 4 0.2 4 5
WARNING: The median and mean for the initial connection time are not within a normal deviation
These results are probably not that reliable.

Percentage of the requests served within a certain time (ms)
50% 4
66% 4
75% 4
80% 4
90% 4
95% 4
98% 5
99% 5
100% 5 (longest request)

==================================================================================

关于上面这个例子的疑问:为什么对同一个 FD 要进行 EPOLLIN 和 EPOLLOUT 之间的切换,同时设置 EPOLLIN | EPOLLOUT 不可以吗?

找到的答案如下:

unexpected extra pollout events from epoll

On Sun, 26 Oct 2008, Paul P wrote:

The way epoll works, is by hooking into the existing kernel poll 
subsystem. It hooks into the poll wakeups, via callback, and it that way 
it knows that "something" is changed. Then it reads the status of a file 
via f_op->poll() to know the status.
What happens is that, /* 答案1 */ if you listen for EPOLLIN|EPOLLOUT, when a packet 
arrives the callback hook is hit, and the file is put into a maybe-ready 
list. Maybe-ready because at the time of the callback, epoll has no clue 
of what happened.

After that, via epoll_wait(), f_op->poll() is called to get the status 
of the file, and since POLLIN|POLLOUT is returned (and since you're 
listening for EPOLLIN|EPOLLOUT), that gets reported back to you.
 The 
POLLOUT event, by meaning a buffer-full->buffer-avail transition, did 
not really happen, but since POLLOUT is true, that gets reported back 
too.


Ok, so make sure I understand you correctly, you're saying that 
currently the kernel doesn't have awareness of the difference between 
EPOLLIN and EPOLLOUT events because at the time of the event, both 
EPOLLIN/EPOLLOUT are returned from the kernel and that at least for the 
near term that's not going to change.
 At some point, we can expect the 
EPOLLOUT to give the correct event, but not till later than .28.

The kernel knows the difference between EPOLLIN and EPOLLOUT, of course. 
At the moment though, such condition is not reported during wakeups, and 
this is what is going to be changing.

通过上面的对话知道,原因就是如果同时设置 EPOLLIN 和 EPOLLOUT ,当事件发生时,EPOLLIN 和 EPOLLOUT 都会被返回。

/* 答案2 */ The best way to do it ATM, is to wait for POLLOUT only when
really needed.


I'm a little unclear how to do this. If I set the epoll_wait call to 
wait for just epollin events, that's fine. But when I send a large 
buffer of data and use epoll_ctl to look for epollin|epollout events, 
don't I have the same problem? 

You do that by writing data until it's finished, or you get EAGAIN. If you 
get EAGAIN, you listen for EPOLLOUT.
Reading is same, but you'd wait for EPOLLIN.

上面这段话给出了如何使用 epoll ,那就是ATM (异步模式),读和写分开在不同的时间,交替进行。

自己写了一个例子,验证上面的说法,如下:

server端:

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>


#define SERVPORT 9527
#define MAXBUF 1024
#define MAXFDS 5000
#define EVENTSIZE 100


ssize_t readn(int fd, void *vptr, size_t n)
{
char *ptr;
size_t nleft;
ssize_t nread;

nleft = n;
ptr = vptr;
while(nleft > 0)
{
if( (nread = read(fd, ptr, nleft)) == -1)
{
if(errno == EINTR)
continue;
else if(errno == EAGAIN)
break;
else
return (-1);

}
else if(nread == 0)
break;

nleft -= nread;
ptr += nread;
}

return (n - nleft);
}

int setnonblocking(int fd)
{
int opts;
if( (opts = fcntl(fd, F_GETFL, 0)) == -1)
{
perror("fcntl");
return -1;
}

opts = opts | O_NONBLOCK;
if( (opts = fcntl(fd, F_SETFL, opts)) == -1)
{
perror("fcntl");
return -1;
}

return 0;
}

int main(void)
{
char buf[MAXBUF];
int len, n;

struct sockaddr_in servaddr;
int sockfd, listenfd, epollfd, nfds;

struct epoll_event ev;
struct epoll_event events[EVENTSIZE];

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERVPORT);

if( (epollfd = epoll_create(MAXFDS)) == -1)
{
perror("epoll");
exit(1);
}

if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}

if(setnonblocking(listenfd) == -1)
{
perror("setnonblocking");
exit(1);
}

if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
perror("bind");
exit(1);
}

if(listen(listenfd, 10) == -1)
{
perror("listen");
exit(1);
}


/* --------------------------------------------*/
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listenfd;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev) == -1)
{
perror("epoll_ctl");
exit(1);
}

for( ; ; )
{
if( (nfds = epoll_wait(epollfd, events, EVENTSIZE, -1)) == -1)
{
perror("epoll_wait");
exit(1);
}

for(n = 0; n < nfds; n++)
{
if(events[n].data.fd == listenfd)
{
while( (sockfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) > 0)
{
if(setnonblocking(sockfd) == -1)
{
perror("setnonblocking");
exit(1);
}
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
ev.data.fd = sockfd;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1)
{
perror("epoll_ctl");
exit(1);
}
}
/*if(sockfd == -1)
{
perror("accept");
exit(1);
}*/
}
else
{
if(events[n].events & EPOLLIN)
{
if( (len = readn(events[n].data.fd, buf, MAXBUF)) == -1)
{
perror("readn");
exit(1);
}
else if(len == 0)
{
close(events[n].data.fd);
ev.data.fd = events[n].data.fd;
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[n].data.fd, &ev);
}

buf[len] = '\0';
printf("received %s\n", buf);
}

if(events[n].events & EPOLLOUT)
{
printf("when EPOLLIN is triggered, EPOLLOUT is also triggered\n");
}
}
}
}
}

client 端:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVPORT 9527
#define MAXBUF 1024

int main(void)
{
char buf[MAXBUF];
struct sockaddr_in servaddr;
int fd;
int n, len;

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
servaddr.sin_port = htons(SERVPORT);

if( (fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}

if(connect(fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
perror("connect");
exit(1);
}

for(n = 0; n < 10; n++)
{
snprintf(buf, MAXBUF, "time %d", n);
len = strlen(buf);
if(write(fd, buf, len) != len)
{
printf("write error");
close(fd);
exit(1);
}

}

close(fd);

return 0;
}

从验证的结果知道:
1. 如果设置 EPOLLOUT ,新连接到来时,EPOLLOUT 被触发。
2. 如果同时设置 EPOLLIN | EPOLLOUT ,EPOLLIN被触发时,EPOLLOUT` 也被触发。

==================================================================================