【整理】I/O复用模型中的 select、poll、epoll_fd活跃连接大 epoll-程序员宅基地

技术标签: 网络编程  


1. select、poll、epoll简介
  epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select:
  select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
      一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
       当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

poll:
  poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
  它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

 2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。


epoll:
  epoll支持水平触发边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
      即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。


select、poll、epoll 区别总结:
1、支持一个进程所能打开的最大连接数


2、FD剧增后带来的IO效率问题


3、 消息传递方式


总结:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

转自:http://blog.csdn.net/klarclm/article/details/8828486

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

  在linux网络编程中,很长的时间都是用select来做事件触发.在linux新内核中,有了一种替换它的机制,就是epoll.
  相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率.因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数据数据越多,自然耗时就越多.

epoll的接口三个函数:

1) int epoll_create(int size);
创建一个epoll句柄,size用来告诉内核这个监听的数据一共有多大.
需要注意的是,当创建好epoll句柄后,它会占用一个fd值,在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽.

2) int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
epoll注册函数,

第一个参数是epoll_create()的返回值.
第二个参数表示动作,用三个宏来表示
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
第三个参数是需要监听的fd
第四个参数是告诉内核需要监听什么事.

struct_event结构如下:

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 */  
};

events可以是以下几个宏的集合:
EPOLLIN: 表示对应的文件描述符可读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读
EPOLLERR 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断
EPOLLET: 将EPOLL设为边缘触发模式(Edge Triggered).这是相对于水平触发(Level Triggered)来说的
EPOLLONSHOT: 只监听一次,当监听玩这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到WPOLL队列里。


3) int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
  参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒),0会立即返回,-1将不确定,也有说法是永久阻塞.该函数返回需要处理的事件数目,如返回0表示已超时.


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


epoll模型
  首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
  之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回为-1的时候表示一直等下去,直到有事件返回,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

epoll_wait范围之后应该是一个循环,遍利所有的事件。

#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;  
  
int main(int argc,char *argv[])  
{  
    int maxi,listenfd,connfd,sockfd,epfd,nfds;  
    ssize_t n;  
    char line[100];  
  
    listenfd = socket(AF_INET,SOCK_STREAM,0);  
  
    //声明epoll_event结构体变量,ev用于注册事件,数组用于回传要处理的事件  
    struct epoll_event ev,events[20];  
    epfd = epoll_create(256);  
    ev.date.fd = listenfd;  
    ev.events = EPOLLIN|EPOLLET;  
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件  
  
    struct sockaddr_in serveraddr;  
    bzero(&serveraddr,sizeof(serveraddr));  
    char *local_addr = "127.0.0.1";  
    inet_aton(local_addr,&(serveraddr.sin_addr));  
    serveraddr.sin_port=htons(8888);  
    bind(listenfd,(sockaddr*)&serveraddr,sizeof(serveraddr));  
    listen(listenfd,LISTENQ);  
    maxi=0;  
  
    for(;;)  
    {  
        //等待epoll事件发生  
        nfds = epoll_wait(epfd,events,20,500);  
  
        //处理发生的所有事件  
        for(int i = 0;i <nfds;i++)  
        {  
            if(events[i].data.fd == listenfd) //如果新监测到一个SOCKET用户连接到了绑定的socket端口,建立新连接  
            {  
                struct sockaddr_in clientaddr;  
                socketlen_t clilen;  
                connfd = accept(listenfd,(sockaddr *)&clientaddr,&clilen);  
                char *str = inet_ntoa(clientaddr.sin_addr);  
                cout <<"accept a connection from "<<str<<endll;  
                  
                ev.data.fd = connfd;  
                ev.events = EPOLLIN|EPOLLET;  
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);  
  
            }  
            else if(events[i].events & EPOLLIN) //如果是以连接的用户,并且收到数据,那么进行读入  
            {  
                sockfd = events[i].data.fd;  
                n = read(sockfd,line,100);  
                line[n] = '\0';  
                cout <<"read msg :"<<line<<endl;  
  
                ev.data.fd = sockfd;  
                ev.events = EPOLLOUT|EPOLLEN;  
                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;  
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  
            }  
  
        }  
    }  
  
    return 0;  
  
  
}

epoll和select,poll区别:
1.  支持一个进程打开大数目的socket描述符(FD)
  select最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降

2.  IO效率不随FD数目增加而线性下降
  传统的select/poll另外一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作——这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。
  epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

3.  使用mmap加速内核与用户空间的消息传递
  无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

4.  内核微调
这一点其实不算epoll的优点了,而是整个Linux平台的优点.

转自:http://blog.csdn.net/felixit0120/article/details/7712207

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

《linux下epoll如何实现高效处理百万句柄的》

  开发高性能网络程序时,windows开发者们言必称iocp,linux开发者们则言必称epoll。大家都明白epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,它到底为什么可以高速处理这么多并发连接呢?

  先简单回顾下如何使用C库封装的3个epoll系统调用吧。

    int epoll_create(int size);  
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  

  使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。

  epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。

  epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。

  从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。

  所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。

  在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。

  epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

static int __init eventpoll_init(void)  
{  
    ... ...  
  
    /* Allocates slab cache used to allocate "struct epitem" items */  
    epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),  
            0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,  
            NULL, NULL);  
  
    /* Allocates slab cache used to allocate "struct eppoll_entry" */  
    pwq_cache = kmem_cache_create("eventpoll_pwq",  
            sizeof(struct eppoll_entry), 0,  
            EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);  
  
 ... ...

  epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点, 在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

  而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!

  那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
  

  如此,一颗红黑树一张准备就绪句柄链表少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

  最后看看epoll独有的两种模式LTET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

  这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。

转自:http://blog.csdn.net/russell_tao/article/details/7160071

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sinat_31135199/article/details/77001692

智能推荐

2023年06月CCF-GESP编程能力等级认证Python编程二级真题解析_2023 年 6 月 gesp python 二级试卷解析-程序员宅基地

文章浏览阅读97次。高级语言编写的程序需要经过以下()操作,可以生成在计算机上运行的可执行代码。A:编辑B:保存C:调试D:编译。_2023 年 6 月 gesp python 二级试卷解析

Jmeter3.0下载与安装-程序员宅基地

文章浏览阅读108次。撸了今年阿里、头条和美团的面试,我有一个重要发现.......>>> ..._jmeter3.0下载

CH9326 USB转串口 从此不用再装驱动_usb转串口双向 免驱动-程序员宅基地

文章浏览阅读5.5k次。概 述CH9326是一款HID 转串口芯片。CH9326支持双向数据传输,用于接收串口数据,并按照HID 类设备规范,将数据打包通过USB口上传给计算机,或者从计算机接收符合HID类设备的USB数据包,并从串口进行发送。通过提供的上位机软件,用户也可自行配置芯片的 VID、 PID,以及各种字符串描述符。下图为其一般应用框图。特 点>支持 12Mbps..._usb转串口双向 免驱动

计算机毕业设计题目大全参考题目-程序员宅基地

文章浏览阅读1k次,点赞12次,收藏9次。基于django及爬虫实现12306网站信息可视化的用户出行推荐系统。基于OpenCV的旅游景区客流量统计及游客身份验证的人脸识别系统。基于Django框架的企业用户信息权限数据管理系统的设计与开发。基于Django框架的文本类小说爬取及可视化数据分析的门户网站。基于Django框架的物流管理及物流追踪系统设计与实现。基于NumPy的学生信息与成绩分析系统的设计与实现。基于flask框架的数码销售及售后平台的设计与实现。基于Django的汽车挂售租赁服务平台的设计与实现。

程序设计综合实习(C语言):考勤管理系统_c语言创建一个职工考勤管理系统-程序员宅基地

文章浏览阅读2.5k次,点赞10次,收藏58次。5.操作实现:实现了增加员工信息、删除员工信息、修改员工信息、开始考勤、输出员工信息、查找员工信息和考勤信息、统计员工出勤率等基本操作。5.实现修改员工信息的功能,先输入需要修改的员工工号,然后选择需要修改的信息,最后将修改后的信息保存。2.实现菜单功能,包括增加员工信息、删除员工信息、修改员工信息、开始考勤、输出员工信息、查找员工信息和考勤信息、统计员工出勤率和退出系统。3.实现增加员工信息的功能,包括输入员工的工号、姓名、年龄、性别、部门等信息,并将其插入到链表的尾部。找到对应的员工信息后,将其输出。_c语言创建一个职工考勤管理系统

unity的CommandBuffer_renderingcommandbuffer: invalid pass index 1 in dr-程序员宅基地

文章浏览阅读2.8k次。https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.htmlDescritptionlist of graphics commands to exectuecommand buffers holds list of rendering commands (set render target, draw mesh…)..._renderingcommandbuffer: invalid pass index 1 in drawmesh

随便推点

第102讲 预定义超全局数组①-原理分析 $_SERVER $_ENV $GLOBALS_$_server 原理-程序员宅基地

文章浏览阅读354次。$_SERVER$HTTP_SERVER_VARS [已弃用]SERVER−−SERVER−−_SERVER -- HTTP_SERVER_VARS [已弃用] — 服务器和执行环境信息 说明 $_SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务..._$_server 原理

未禁用nouveau导致Ubuntu安装Cuda的runfile安装方法出错:[ERROR]: Install of 455.32.00 failed, quitting_[error]: install of driver component failed. [erro-程序员宅基地

文章浏览阅读1w次,点赞6次,收藏55次。很多朋友在给Ubuntu(Linux)安装Cuda时,参考官方安装步骤导致安装出错:wget https://developer.download.nvidia.com/compute/cuda/11.1.1/local_installers/cuda_11.1.1_455.32.00_linux.runsudo sh cuda_11.1.1_455.32.00_linux.run在sudo sh后会出现如下错误:Installation failed. See log at /var/l_[error]: install of driver component failed. [error]: install of 455.32.00 f

android AndFix热补丁框架(不发版本,修复线上bug),2024年最新面试官的犀利问题-程序员宅基地

文章浏览阅读948次,点赞23次,收藏11次。其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

matplotlib柱状图轴标签自定义排序_python matplotlib 画柱状图怎么改变坐标的顺序-程序员宅基地

文章浏览阅读6.4k次,点赞2次,收藏16次。昨天一位同学提问 “matplotlib 画柱状图时,横坐标是从表格中指定列获取的,如何设置横坐标的顺序呢?” 原始数据结构如下图所示,需要对学历分组求平均工资后画柱状图,顺序应为按学历由低到高,即 ['大专', '本科', '硕士', '博士']_python matplotlib 画柱状图怎么改变坐标的顺序

5款项目管理软件_个人工作项目管理软件-程序员宅基地

文章浏览阅读99次。​1、国内敏捷研发项目管理软件Leangoo领歌.Leangoo领歌 - 高效企业必备的敏捷工具,Scrum工具,SAFe敏捷工具,敏捷项目管理,敏捷研发工具​_个人工作项目管理软件

jsp中使用POST的方法在网页之间传递参数的简单方法_jsp post-程序员宅基地

文章浏览阅读1.6w次,点赞3次,收藏2次。从inputtext.jsp跳转到inputtext1res.jsp,同时将输入框的值送post的方式传递过去定义输入框的默认值以及点击事件在inputtext1res.jsp当中使用request.getParameter(”输入框的name”)即可获取到数值 ,如果要获取同一name的多个值可以使用reques.getParameterValues(“输入框的name”)input的其他类_jsp post

推荐文章

热门文章

相关标签