epoll

epoll
2010年10月09日
  I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。在linux2.6内核中,有了一种替换它的机制,就是epoll。
  一、epoll相关的数据结构和函数
  epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数。
  1、数据结构
  typedef union epoll_data {
  void *ptr;
  int fd;
  __uint32_t u32;
  __uint64_t u64;
  } epoll_data_t;
  struct epoll_event {
  __uint32_t events; /* Epoll events */
  epoll_data_t data; /* User data variable */
  };
  结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件,其中epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件可能的取值。events可以是以下几个宏的集合:
  EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  EPOLLOUT:表示对应的文件描述符可以写;
  EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  EPOLLERR:表示对应的文件描述符发生错误;
  EPOLLHUP:表示对应的文件描述符被挂断;
  EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
  EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
  2、函数
  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);
  该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
  参数:epfd:由 epoll_create 生成的epoll专用的文件描述符;
  op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
  fd:关联的文件描述符;
  event:指向epoll_event的指针。
  返回值:如果调用成功返回0,不成功返回-1
  (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表示已超时。
  二、epoll使用方法
  这是epoll的man手册提供的一个例子,这段代码假设一个非阻塞的socket监听listener被建立并且一个epoll句柄kdpfd已经提前用epoll_create建立了:
  [CODE]
  struct epoll_event ev, *events;
  for(;;) {
  nfds = epoll_wait(kdpfd, events, maxevents, -1);/*wait for an I/O event.
  for(n = 0; n
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #define MAXLINE 10
  #define OPEN_MAX 100
  #define LISTENQ 20
  #define SERV_PORT 5555
  #define INFTIM 1000 void setnonblocking(int sock) { int opts; opts=fcntl(sock,F_GETFL); if(opts注册事件,数组用于回传要处理的事件
  struct epoll_event ev,events[20];
  //生成用于处理accept的epoll专用的文件描述符
  epfd=epoll_create(256);
  struct sockaddr_in clientaddr;
  struct sockaddr_in serveraddr;
  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  //把socket设置为非阻塞方式
  setnonblocking(listenfd);
  //设置与要处理的事件相关的文件描述符
  ev.data.fd=listenfd;
  //设置要处理的事件类型
  ev.events=EPOLLIN|EPOLLET;
  //注册epoll事件
  epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
  bzero(&serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  char *local_addr="200.200.200.204";
  inet_aton(local_addr,&(serveraddr.sin_addr));//hto ns(SERV_PORT);
  serveraddr.sin_port=htons(SERV_PORT);
  bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
  listen(listenfd, LISTENQ);
  maxi = 0;
  for ( ; ; ) {
  //等待epoll事件的发生
  nfds=epoll_wait(epfd,events,20,500);
  //处理所发生的所有事件
  for(i=0;i注册ev
  epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
  }
  else if(events[i].events&EPOLLIN)
  {
  if ( (sockfd = events[i].data.fd) 解决方案(传统的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之上了。下面用一个生活中的例子解释这点。
  假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面。如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的。现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色。
  进一步解释select和epoll模型的差异。select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:
  int n = select(&readset,NULL,NULL,100);
  for (int i = 0; n > 0; ++i)
  {
  if (FD_ISSET(fdarray[i], &readset))
  {
  do_something(fdarray[i]);
  --n;
  }
  }
  epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:
  n=epoll_wait(epfd,events,20,500);
  for(i=0;i1),但是监听socket只accept了一个连接,那么其它未accept的连接将不会在ET模式下给监听socket发出通知,此时状态不发生变化;对于一般的socket,如果对应的缓冲区本身已经有了N字节的数据,而只取出了小于N字节的数据,那么残存的数据不会造成状态发生变化.
  2)对于监听可写事件时,同理可推,不再详述.
  3)而不论是监听可读还是可写,对方关闭socket连接都将造成状态发生变化。例如客户主动中断了socket连接,那么都将造成server端发生状态的变化,从而server得到通知,将已经在本方缓冲区中的数据读出。
  总结如下:仅当对方的动作(发出数据,关闭连接等)造成的事件才能导致状态发生变化,而本方协议栈中已经处理的事件(包括接收了对方的数据,接收了对方的主动连接请求)并不是造成状态发生变化的必要条件,状态变化一定是对方造成的.所以在ET模式下的,必须一直处理到出错或者完全处理完毕,才能进行下一个动作,否则可能会发生错误。
  (3)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:
  while(rs)
  {
  buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
  if(buflen
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  using namespace std;
  #define MAXLINE 5
  #define OPEN_MAX 100
  #define LISTENQ 20
  #define SERV_PORT 5000
  #define INFTIM 1000
  void setnonblocking(int sock)
  {
  int opts;
  opts=fcntl(sock,F_GETFL);
  if(opts注册事件,数组用于回传要处理的事件
  struct epoll_event ev,events[20];
  //生成用于处理accept的epoll专用的文件描述符
  epfd=epoll_create(256);
  struct sockaddr_in clientaddr;
  struct sockaddr_in serveraddr;
  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  //把socket设置为非阻塞方式
  setnonblocking(listenfd);
  //设置与要处理的事件相关的文件描述符
  ev.data.fd=listenfd;
  //设置要处理的事件类型
  ev.events=EPOLLIN|EPOLLET;
  //ev.events=EPOLLIN;
  //注册epoll事件
  epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
  bzero(&serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  char *local_addr="127.0.0.1";
  inet_aton(local_addr,&(serveraddr.sin_addr));
  serveraddr.sin_port=htons(SERV_PORT);
  bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
  listen(listenfd, LISTENQ);
  maxi = 0;
  for ( ; ; ) {
  //等待epoll事件的发生
  nfds=epoll_wait(epfd,events,20,500);
  //处理所发生的所有事件
  for(i=0;i注册ev
  epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
  }
  else if(events[i].events&EPOLLIN)
  {
  printf("EPOLLIN\n");
  if ( (sockfd = events[i].data.fd) 循环, 也就是在发送完之后连接的状态不发生改变--既不再发送数据, 也不关闭连接,这样才能观察出server的状态:
  #!/usr/bin/perl
  use IO::Socket;
  my $host = "127.0.0.1";
  my $port = 5000;
  my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
  my $msg_out = "1234567890";
  print $socket $msg_out;
  print "now send over, go to sleep\n";
  while (1)
  {
  sleep(1);
  }
  运行server和client发现,server仅仅读取了5字节的数据,而client其实发送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事件了.
  如果我们把client改为这样:
  #!/usr/bin/perl
  use IO::Socket;
  my $host = "127.0.0.1";
  my $port = 5000;
  my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
  my $msg_out = "1234567890";
  print $socket $msg_out;
  print "now send over, go to sleep\n";
  sleep(5);
  print "5 second gonesend another line\n";
  print $socket $msg_out;
  while (1)
  {
  sleep(1);
  } 可以发现,在server接收完5字节的数据之后一直监听不到client的事件,而当client休眠5秒之后重新发送数据,server再次监听到了变化,只不过因为只是读取了5个字节,仍然有10个字节的数据(client第二次发送的数据)没有接收完.
  如果上面的实验中,对accept的socket都采用的是LT模式,那么只要还有数据留在buffer中,server就会继续得到通知,读者可以自行改动代码进行实验.
  基于这两个实验,可以得出这样的结论:ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.
  参考资料:
  1.http://hi.baidu.com/scrich99/blog/item/4c6b013f4a4 7d40bbba16731.html
  2.http://hi.baidu.com/scrich99/blog/item/43379d391d2 ccce114cecb37.html
  3.http://hi.baidu.com/scrich99/blog/item/87b3a42baa6 fe5f598250a30.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值