Epoll工作模式详解

我们目前的网络模型大都是epoll的,因为epoll模型会比select模型性能高很多, 尤其在大连接数的情况下,作为后台开发人员需要理解其中的原因。

select/epoll的特点

select的特点:select 选择句柄的时候,是遍历所有句柄,也就是说句柄有事件响应时,select需要遍历所有句柄才能获取到哪些句柄有事件通知,因此效率是非常低。但是如果连接很少的情况下, select和epoll的LT触发模式相比, 性能上差别不大。
这里要多说一句,select支持的句柄数是有限制的, 同时只支持1024个,这个是句柄集合限制的,如果超过这个限制,很可能导致溢出,而且非常不容易发现问题, TAF就出现过这个问题, 调试了n天,才发现:)当然可以通过修改linux的socket内核调整这个参数。
epoll的特点:epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高,内核将句柄用红黑树保存的。
对于epoll而言还有ET和LT的区别,LT表示水平触发,ET表示边缘触发,两者在性能以及代码实现上差别也是非常大的。

epoll的LT和ET的区别

LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
下面举一个列子来说明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采用LT模式下, 如果accept调用有返回就可以马上建立当前这个连接了,再epoll_wait等待下次通知,和select一样。
但是对于ET而言,如果accpet调用有返回,除了建立当前这个连接外,不能马上就epoll_wait还需要继续循环accpet,直到返回-1,且errno==EAGAIN,TAF里面的示例代码:

if(ev.events& EPOLLIN)
{
    do
    {
        struct sockaddr_in stSockAddr;
        socklen_t iSockAddrSize = sizeof(sockaddr_in);
        TC_Socket cs;
        cs.setOwner(false);
        //接收连接
        TC_Socket s;
        s.init(fd, false, AF_INET);
        int iRetCode = s.accept(cs, (struct sockaddr*) &stSockAddr, iSockAddrSize);
        if (iRetCode > 0)
        {
            ...建立连接
        }
        else
        {
            //直到发生EAGAIN才不继续accept
            if(errno == EAGAIN)
            {
                break;
            }
        }
    }while(true);
}

同样,recv/send等函数, 都需要到errno==EAGAIN

从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。

epoll ET详解

ET模型的逻辑:内核的读buffer有内核态主动变化时,内核会通知你, 无需再去mod。写事件是给用户使用的,最开始add之后,内核都不会通知你了,你可以强制写数据(直到EAGAIN或者实际字节数小于 需要写的字节数),当然你可以主动mod OUT,此时如果句柄可以写了(send buffer有空间),内核就通知你。
这里内核态主动的意思是:内核从网络接收了数据放入了读buffer(会通知用户IN事件,即用户可以recv数据)
并且这种通知只会通知一次,如果这次处理(recv)没有到刚才说的两种情况(EAGIN或者实际字节数小于 需要读写的字节数),则该事件会被丢弃,直到下次buffer发生变化。
与LT的差别就在这里体现,LT在这种情况下,事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。

另外对于ET而言,当然也不一定非send/recv到前面所述的结束条件才结束,用户可以自己随时控制,即用户可以在自己认为合适的时候去设置IN和OUT事件:
1 如果用户主动epoll_mod OUT事件,此时只要该句柄可以发送数据(发送buffer不满),则epoll
_wait就会响应(有时候采用该机制通知epoll_wai醒过来)。
2 如果用户主动epoll_mod IN事件,只要该句柄还有数据可以读,则epoll_wait会响应。
这种逻辑在普通的服务里面都不需要,可能在某些特殊的情况需要。 但是请注意,如果每次调用的时候都去epoll mod将显著降低效率,已经吃过几次亏了!

因此采用et写服务框架的时候,最简单的处理就是:
建立连接的时候epoll_add IN和OUT事件, 后面就不需要管了
每次read/write的时候,到两种情况下结束:
1 发生EAGAIN
2 read/write的实际字节数小于 需要读写的字节数
对于第二点需要注意两点:
A:如果是UDP服务,处理就不完全是这样,必须要recv到发生EAGAIN为止,否则就丢失事件了
因为UDP和TCP不同,是有边界的,每次接收一定是一个完整的UDP包,当然recv的buffer需要至少大于一个UDP包的大小
随便再说一下,一个UDP包到底应该多大?
对于internet,由于MTU的限制,UDP包的大小不要超过576个字节,否则容易被分包,对于公司的IDC环境,建议不要超过1472,否则也比较容易分包。

B 如果发送方发送完数据以后,就close连接,这个时候如果recv到数据是实际字节数小于读写字节数,根据开始所述就认为到EAGIN了从而直接返回,等待下一次事件,这样是有问题的,close事件丢失了!
因此如果依赖这种关闭逻辑的服务,必须接收数据到EAGIN为止,例如lb。

名词解释:man epoll之后,得到如下结果:

NAME
epoll - I/O event notification facility

SYNOPSIS
#include <sys/epoll.h>

DESCRIPTION
epoll is a variant of poll(2) that can be used either as Edge or Level
Triggered interface and scales well to large numbers of watched fds.
Three system calls are provided to set up and control an epoll set:
epoll_create(2), epoll_ctl(2), epoll_wait(2).

An epoll set is connected to a file descriptor created by epoll_cre-
ate(2). Interest for certain file descriptors is then registered via
epoll_ctl(2). Finally, the actual wait is started by epoll_wait(2).

其实,一切的解释都是多余的,按照我目前的了解,EPOLL模型似乎只有一种格式,所以大家只要参考我下面的代码,就能够对EPOLL有所了解了,代码的解释都已经在注释中:

while (TRUE)
{
int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);//等待EPOLL时间的发生,相当于监听,至于相关的端口,需要在初始化EPOLL的时候绑定。
if (nfds <= 0)
continue;
m_bOnTimeChecking = FALSE;
G_CurTime = time(NULL);
for (int i=0; i<nfds; i++)
{
try
{
if (m_events[i].data.fd == m_listen_http_fd)//如果新监测到一个HTTP用户连接到绑定的HTTP端口,建立新的连接。由于我们新采用了SOCKET连接,所以基本没用。
{
OnAcceptHttpEpoll ();
}
else if (m_events[i].data.fd == m_listen_sock_fd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
{
OnAcceptSockEpoll ();
}
else if (m_events[i].events & EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
{
OnReadEpoll (i);
}

OnWriteEpoll (i);//查看当前的活动连接是否有需要写出的数据。
}
catch (int)
{
PRINTF ("CATCH捕获错误/n");
continue;
}
}
m_bOnTimeChecking = TRUE;
OnTimer ();//进行一些定时的操作,主要就是删除一些短线用户等。
}

 其实EPOLL的精华,按照我目前的理解,也就是上述的几段短短的代码,看来时代真的不同了,以前如何接受大量用户连接的问题,现在却被如此轻松的搞定,真是让人不得不感叹。

今天搞了一天的epoll,想做一个高并发的代理程序。刚开始真是郁闷,一直搞不通,网上也有几篇介绍epoll的文章。但都不深入,没有将一些注意的地方讲明。以至于走了很多弯路,现将自己的一些理解共享给大家,以少走弯路。

epoll用到的所有函数都是在头文件sys/epoll.h中声明,有什么地方不明白或函数忘记了可以去看一下。
epoll和select相比,最大不同在于:

1epoll返回时已经明确的知道哪个sokcet fd发生了事件,不用再一个个比对。这样就提高了效率。
2select的FD_SETSIZE是有限止的,而epoll是没有限止的只与系统资源有关。


1、epoll_create函数
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。随你定好了。只要你有空间。可参见上面与select之不同2.

22、epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除

fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-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 */
};


如:
struct epoll_event ev;
//设置与要处理的事件相关的文件描述符
ev.data.fd=listenfd;
//设置要处理的事件类型
ev.events=EPOLLIN|EPOLLET;
//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);


常用的事件类型:
EPOLLIN :表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符有事件发生;


3、epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可
返回发生事件数。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <errno.h>
  4. #include <string.h>
  5. #include <sys/types.h>
  6. #include <netinet/in.h>
  7. #include <sys/socket.h>
  8. #include <sys/wait.h>
  9. #include <unistd.h>
  10. #include <arpa/inet.h>
  11. #include <openssl/ssl.h>
  12. #include <openssl/err.h>
  13. #include <fcntl.h>
  14. #include <sys/epoll.h>
  15. #include <sys/time.h>
  16. #include <sys/resource.h>

  1. #define MAXBUF 1024
  2. #define MAXEPOLLSIZE 10000

  1. /*
  2. setnonblocking - 设置句柄为非阻塞方式
  3. */
  4. int setnonblocking(int sockfd)
  5. {
  6. if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
  7. {
  8. return -1;
  9. }
  10. return 0;
  11. }

  1. /*
  2. handle_message - 处理每个 socket 上的消息收发
  3. */
  4. int handle_message(int new_fd)
  5. {
  6. char buf[MAXBUF + 1];
  7. int len;
  8. /* 开始处理每个新连接上的数据收发 */
  9. bzero(buf, MAXBUF + 1);
  10. /* 接收客户端的消息 */
  11. len = recv(new_fd, buf, MAXBUF, 0);
  12. if (len > 0)
  13. {
  14. printf
  15. ("%d接收消息成功:'%s',共%d个字节的数据/n",
  16. new_fd, buf, len);
  17. }
  18. else
  19. {
  20. if (len < 0)
  21. printf
  22. ("消息接收失败!错误代码是%d,错误信息是'%s'/n",
  23. errno, strerror(errno));
  24. close(new_fd);
  25. return -1;
  26. }
  27. /* 处理每个新连接上的数据收发结束 */
  28. return len;
  29. }
  30. /************关于本文档********************************************
  31. *filename: epoll-server.c
  32. *purpose: 演示epoll处理海量socket连接的方法
  33. *wrote by: zhoulifa(<a href="mailto:zhoulifa@163.com">zhoulifa@163.com</a>) 周立发(<a href="http://zhoulifa.bokee.com">http://zhoulifa.bokee.com</a>)
  34. Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言
  35. *date time:2007-01-31 21:00
  36. *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
  37. * 但请遵循GPL
  38. *Thanks to:Google
  39. *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
  40. * 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!
  41. *********************************************************************/
  42. int main(int argc, char **argv)
  43. {
  44. int listener, new_fd, kdpfd, nfds, n, ret, curfds;
  45. socklen_t len;
  46. struct sockaddr_in my_addr, their_addr;
  47. unsigned int myport, lisnum;
  48. struct epoll_event ev;
  49. struct epoll_event events[MAXEPOLLSIZE];
  50. struct rlimit rt;
  51. myport = 5000;
  52. lisnum = 2;
  53. /* 设置每个进程允许打开的最大文件数 */
  54. rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
  55. if (setrlimit(RLIMIT_NOFILE, &rt) == -1)
  56. {
  57. perror("setrlimit");
  58. exit(1);
  59. }
  60. else
  61. {
  62. printf("设置系统资源参数成功!/n");
  63. }

  1. /* 开启 socket 监听 */
  2. if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1)
  3. {
  4. perror("socket");
  5. exit(1);
  6. }
  7. else
  8. {
  9. printf("socket 创建成功!/n");
  10. }
  11. setnonblocking(listener);

  1. bzero(&my_addr, sizeof(my_addr));
  2. my_addr.sin_family = PF_INET;
  3. my_addr.sin_port = htons(myport);
  4. my_addr.sin_addr.s_addr = INADDR_ANY;

  1. if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
  2. {
  3. perror("bind");
  4. exit(1);
  5. }
  6. else
  7. {
  8. printf("IP 地址和端口绑定成功/n");
  9. }
  10. if (listen(listener, lisnum) == -1)
  11. {
  12. perror("listen");
  13. exit(1);
  14. }
  15. else
  16. {
  17. printf("开启服务成功!/n");
  18. }
  19. /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */
  20. kdpfd = epoll_create(MAXEPOLLSIZE);
  21. len = sizeof(struct sockaddr_in);
  22. ev.events = EPOLLIN | EPOLLET;
  23. ev.data.fd = listener;
  24. if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0)
  25. {
  26. fprintf(stderr, "epoll set insertion error: fd=%d/n", listener);
  27. return -1;
  28. }
  29. else
  30. {
  31. printf("监听 socket 加入 epoll 成功!/n");
  32. }
  33. curfds = 1;
  34. while (1)
  35. {
  36. /* 等待有事件发生 */
  37. nfds = epoll_wait(kdpfd, events, curfds, -1);
  38. if (nfds == -1)
  39. {
  40. perror("epoll_wait");
  41. break;
  42. }
  43. /* 处理所有事件 */
  44. for (n = 0; n < nfds; ++n)
  45. {
  46. if (events[n].data.fd == listener)
  47. {
  48. new_fd = accept(listener, (struct sockaddr *) &their_addr,&len);
  49. if (new_fd < 0)
  50. {
  51. perror("accept");
  52. continue;
  53. }
  54. else
  55. {
  56. printf("有连接来自于: %d:%d, 分配的 socket 为:%d/n",
  57. inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
  58. }
  59. setnonblocking(new_fd);
  60. ev.events = EPOLLIN | EPOLLET;
  61. ev.data.fd = new_fd;
  62. if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0)
  63. {
  64. fprintf(stderr, "把 socket '%d' 加入 epoll 失败!%s/n",
  65. new_fd, strerror(errno));
  66. return -1;
  67. }
  68. curfds++;
  69. }
  70. else
  71. {
  72. ret = handle_message(events[n].data.fd);
  73. if (ret < 1 && errno != 11)
  74. {
  75. epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,&ev);
  76. curfds--;
  77. }
  78. }
  79. }
  80. }
  81. close(listener);
  82. return 0;
  83. }

epoll_wait运行的原理是 等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。 并且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值