linux-epoll研究 - Geek_Ma - 博客园

linux-epoll研究

做linux网络编程的同学都清楚,2.6版本以前的linux内核大多都是用select作为非阻塞的事件触发模型,但是效率低,使用受限已经很明显的暴露了select(包括poll)的缺陷了,为了解决这些缺陷,epoll作为linux新的事件触发模型被创造出来。

一、epoll相对于select的优点:

1.支持一个进程socket描述符(FD)的最大数目

select支持的单进程socket描述符最大数目只有几千,而epoll支持的数目很大,等于系统最大打开的文件描述符数,这个文件描述符数跟内存有一定关系

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

select对事件的扫描是针对于所有创建的socket描述符进行的,也就是说,有多少个socket描述符,就需要遍历多少个句柄,所以IO效率是随描述符增加线性下降的;而epoll只遍历活跃的socket描述符,这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会。比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

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

select事件触发后会将信息从内核拷贝到用户空间,这种拷贝就影响了效率。而mmap将内核与用户空间的内存映射到一块内存上,内核将消息捕获后放入该内存空间,用户无需拷贝直接可以访问,减少了拷贝次数,提高了效率。

二、epoll工作模型

epoll事件有两种模型:
Edge Triggered (ET),边缘触发是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
Level Triggered (LT),水平触发是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

三、值得注意的情况:

1.当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:

while(rs)
{buflen = recv(events[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break;elsereturn;}else if(buflen == 0){// 这里表示对端的socket已正常关闭.
    }if(buflen == sizeof(buf)rs = 1;   // 需要再次读取elsers = 0;
}

2.如果发送端流量大于接收端的流量,也就是说,epoll所在的程序读比转发的socket要慢,由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考mansend),同时,不理会这次请求发送的数据。所以,需要封装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;
}

四、实例

#include <iostream>
#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 <errno.h>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<0){perror("fcntl(sock,GETFL)");exit(1);}opts = opts|O_NONBLOCK;if(fcntl(sock,F_SETFL,opts)<0){perror("fcntl(sock,SETFL,opts)");exit(1);}
}int main()
{int i, maxi, listenfd,connfd, sockfd,epfd,nfds;ssize_t n;char line[MAXLINE];socklen_t clilen;//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件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;//设置要处理的事件类型ETev.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));//htons(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 < nfds;++i){if(events[i].data.fd == listenfd){connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen);if(connfd < 0){perror("connfd<0");exit(1);}//setnonblocking(connfd);char *str = inet_ntoa(clientaddr.sin_addr);cout << "accapt a connection from " << str << endl;//设置用于读操作的文件描述符ev.data.fd = connfd;//设置用于注测的读操作事件ev.events = EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注册evepoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);}else if(events[i].events&EPOLLIN){cout << "EPOLLIN" << endl;if ( (sockfd = events[i].data.fd) < 0)continue;if ( (n = read(sockfd, line, MAXLINE)) < 0){if (errno == ECONNRESET) {close(sockfd);events[i].data.fd = -1;} elsestd::cout<<"readline error"<<std::endl;} else if (n == 0) {close(sockfd);events[i].data.fd = -1;}line[n] = '\0';cout << "read " << line << endl;//设置用于写操作的文件描述符ev.data.fd = sockfd;//设置用于注册的写操作事件ev.events = EPOLLOUT|EPOLLET;//修改sockfd上要处理的事件为EPOLLOUT//epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
            }else if(events[i].events&EPOLLOUT){  sockfd = events[i].data.fd;write(sockfd, line, n);//设置用于读操作的文件描述符ev.data.fd = sockfd;//设置用于注测的读操作事件ev.events = EPOLLIN|EPOLLET;//修改sockfd上要处理的事件为EPOLINepoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);}}}return 0;
}

上面的代码是ET模式

测试脚本1:

#!/usr/bin/python
import socket
import timesock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 5000))sock.send('1234567890')
time.sleep(5)while(1):time.sleep(1)

输出1:

accapt a connection from 0.0.0.0
EPOLLIN
read 12345

说明1:

运行server和client发现,server仅仅读取了5字节的数据,而client其实发送了10字节的数据,也就是说,server仅当第一次监听到了EPOLLIN事件,由于没有读取完数据,而且采用的是ET模式,状态在此之后不发生变化,因此server再也接收不到EPOLLIN事件了。当关闭客户端时,会另外触发一个事件,这个事件又触发了一次读操作,也就将后面的5个字节读取出来。

测试脚本2:

#!/usr/bin/python
import socket
import timesock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 5000))sock.send('1234567890')
time.sleep(5)
sock.send('1234567890')while(1):time.sleep(1)

输出2:

accapt a connection from 0.0.0.0
EPOLLIN
read 12345
(5 sec...)
EPOLLIN
read 67890

说明2:

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

如果上面的实验中,对accept的socket都采用的是LT模式,那么只要还有数据留在buffer中,server就会继续得到通知,可以将上面标黄的选项去掉则变为LT模式。

五、总结

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模式下的,必须一直处理到出错或者完全处理完毕,才能进行下一个动作,否则可能会发生错误。

部分转自他处-没有找到最终来源

linux-epoll研究相关推荐

  1. Linux技术研究-基础篇(raid与LVM,配额)

    Linux技术研究-基础篇(raid与LVM,配额) 创建RAID-5 若想建立新的md1设备 只在/dev下建立还不够 重启后会消失 固化的方法是 为了使udev自动产生/dev/md1, /dev ...

  2. linux epoll监听套接字实例

    linux epoll机制用于IO多路复用,能够同时监听多个接字,使用起来比较简单. 相关接口: #include <sys/epoll.h>int epoll_create(int si ...

  3. 劫起|再谈Linux epoll惊群问题的原因和解决方案

    原作者:dog250,授权发布 重新整理: 极客重生 文章有点长,可以三连收藏慢慢看 缘起 近期排查了一个问题,epoll惊群的问题,起初我并不认为这是惊群导致,因为从现象上看,只是体现了CPU不均衡 ...

  4. linux epoll 开发指南-【ffrpc源码解析】

    linux epoll 开发指南-[ffrpc源码解析] 摘要 关于epoll的问题很早就像写文章讲讲自己的看法,但是由于ffrpc一直没有完工,所以也就拖下来了.Epoll主要在服务器编程中使用,本 ...

  5. Linux epoll的用法

    Linux epoll的用法 epollfd_create函数 #include <sys/epoll.h>int epoll_create (int __size) 参数 含义 __si ...

  6. Linux技术研究-基础篇(启动和自动挂载)

    Linux技术研究-基础篇(启动和自动挂载) 系统启动流程 如果有一天你的服务器启动不了,面对屏幕上的各种各样的提示素手无策. 你不知道服务器出了什么问题,无法判断启动到了哪个环节. 若想排查出问题原 ...

  7. python网络编程linux pdf_Python网络编程:Linux epoll

    原文地址:http://scotdoyle.com/python-epoll-howto.html 介绍 Python已于2.6版本添加访问Linux epoll库的API.这篇教程使用Python ...

  8. [转] Windows完成端口与Linux epoll技术简介

    Windows完成端口与Linux epoll技术简介 2008-01-03 16:18 WINDOWS完成端口编程1.基本概念 2.WINDOWS完成端口的特点 3.完成端口(Completion ...

  9. 再谈Linux epoll惊群问题的原因和解决方案

    差别是什么?差别只是西装! 缘起 近期排查了一个问题,epoll惊群的问题,起初我并不认为这是惊群导致,因为从现象上看,只是体现了CPU不均衡.一共fork了20个Server进程,在请求负载中等的时 ...

  10. 基于linux epoll网络编程细节处理丨epoll原理剖析

    epoll原理剖析以及三握四挥的处理 1. epoll原理详解 2. 连接的创建与断开 3. epoll如何连接细节问题 视频讲解如下,点击观看: 基于linux epoll网络编程细节处理丨epol ...

最新文章

  1. WF流程设计器升级说明
  2. Unit Testing for WinForm
  3. 5G NR 同步过程
  4. Windows之Wireshake之抓HTTP请求包(过滤目的IP)
  5. 【免费毕设】asp.netERP客户关系系统设计(源代码+lunwen)
  6. linux安装mysql5.6rpm_centos6.5 下安装mysql5.6,rpm方式
  7. JVM学习(1)——通过实例总结Java虚拟机的运行机制
  8. 优启通制作系统u盘_EasyU优启通U盘启动盘制作工具BIOS+UEFI双启无捆绑
  9. Rhino6.5软件安装教程|兼容WIN10
  10. [C语言] [游戏] 扫雷
  11. beyond compare 4 This license key has been revoked 解决办法
  12. 微信小程序开发学习5(自定义组件)
  13. 深入理解计算机大端与小端
  14. 繁荣国家数学教育,坚持“知识共享”许可原则
  15. html无法获取图片高宽,如何解决谷歌浏览器下jquery无法获取图片的尺寸
  16. 基于matlab的pcm设计实验报告,基于MATLAB的PCM系统仿真课程设计
  17. 写跨文化交际的论文,有哪些好的题目推荐?
  18. 高德地图API JS实现获取坐标和回显点标记
  19. 计算机中真值的概念,真值和机器数概念
  20. InstallShield 2008 And 脚本(十)

热门文章

  1. java 更新文件内容吗_java Io流更新文件内容
  2. 【测试面试题】偶数和奇数
  3. Python PIL.Image和OpenCV图像格式相互转换
  4. 树莓派cpu检测_【树莓派3B+测评】线程的挂起与恢复CPU温度检测
  5. c语言指数pow,C语言中的指数函数pow()问题
  6. 2015 计算机考研大纲,2015年考研计算机大纲详解:操作系统
  7. php如何使用代码清除bom,使用php清除bom示例
  8. E数据结构实验之查找五:平方之哈希表
  9. 数据结构实验之链表八:Farey序列
  10. 《STL源码剖析》学习-- 1.9-- 可能令你困惑的C++语法2