当前位置: 首页 > news >正文

I/O复用--浅谈epoll

我们聊了聊select和poll知道:

  • 它们都是采取轮询的方式查找是否有就绪描述符。
  • 都有数据结构从用户态拷贝到内核态,内核态拷贝到用户态这个过程。

为了针对许多大量连接,高并发的的场景下大量的资源消耗,效率低的问题,这一节就浅浅来聊一下epoll,epoll是之前的select和poll的增强版本,是linux操作系统独有的I/O复用技术。

对于epoll来说他更灵活,解决了select和poll的弊端,使用起来也更加方便顺手,他不像select和poll那样只提供了一个方法,epoll提供了一组方法。

本节呢就是聊聊epoll的使用和一些优点,对于epoll的两种触犯机制ET和LT的探讨会放在下一节去聊聊,注意select和poll只是LT。

好了~言归正传

目录

epoll的工作原理:

epoll的API:

epoll的特点:

用epoll实现tcp服务器的并发


epoll的工作原理:

  • 1. 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
  • 2. 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
  • 3. 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

用大白话去理解就是,比如你是老师,你给同学们布置了作业,你在班里要检查作业的时候 不是你一个个去问,“啊,你写完了没有啊?”,而是你坐在讲台上,谁写完了,谁把作业拿上来给你,比方说有三个人写完了,这三个人就把作业交给你,你就会知道“哦,有三个人写完了”,至于剩下没交给你的那就是没写完的,你也不需要去问~  

大概就是这样了,关键就是不用你去一个个问,而是谁完了,谁通知你

前面说了epoll提供了一组api,方便我们去使用,下来我们看看~

epoll的API:

epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表组成。

【1. 创建内核事件表:】

函数原型为:

#include <sys/epoll.h>
int epoll_create(int size)
          //成功返回内核事件表的标识符,失败返回-1

功能:该函数 创建内核事件表用于存放描述符和关注的事件。调用这个函数的时候,在内核cache里建立了红黑树struct rb_root用于存储以后epoll_ctl传来的socket,也就是内核事件表;还建立了一个双向链表struct list_headrdllidt用于存储就绪事件。 当epoll_wait调用时,仅仅观察这个双向链表里有没有数据即可,有数据表示有就绪事件,直接返回。
size参数:表示创建的事件表需要多大,记住最后要close关闭,如果不关闭,那么就会导致fd被耗尽

注意:

size参数告诉内核这个epoll对象会处理的事件⼤致数量,⽽不是能够处理的事件的最⼤数(同时,size不要传0,会报invalid argument错误)。
在现在linux版本中,这个size参数已经没有意义了;
返回: epoll对象句柄;之后针对该epoll的操作需要通过该句柄来标识该epoll对象;

【2. 管理内核事件表:】

可以对要监听的事件进行操作:注册,删除,修改

返回: 0表示成功, -1表示错误,根据errno错误码判断错误类型。

原型如下:

#include <sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
                 //成功返回0,失败返回-1.

 参数:

  • epfd:内核事件表的标识符。
  • op:标识操作,有:添加,删除,修改。不用自己写了,根据第二个op参数来选择
EPOLL_CTL_ADD //注册新的文件描述符到内核事件表epfd中
EPOLL_CTL_DEL //从内核事件表中删除文件描述符
EPOLL_CTL_MOD //修改文件描述符
  • fd:要操作的文件描述符。
  • events:保存事件类型,用户填充告诉内核要对哪种事件进行操作。所以需要先定义event结构体,然后再进行操作,表示对何种事件类型进行何种操作
struct epoll_event
{
   short events;//事件类型:在每一个poll的事件类型标识前加个‘E’
   Union epoll_data_t data(联合体,用到其中的fd成员即文件描述符,其他的不用)
}
typedef union epoll_data {
    void *ptr; /* 指向用户自定义数据 */
    int fd; /* 注册的文件描述符 */
    uint32_t u32; /* 32-bit integer */
    uint64_t u64; /* 64-bit integer */
} epoll_data_t;

event.events 取值:

EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读事件,因为需要处理发送来的FIN包; FIN包就是read 返回 0)
EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起⾮阻塞tcp连接,连接建⽴成功事件相当于可写事件)
EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
EPOLLPRI 表示连接上有紧急数据需要读
EPOLLERR 表示连接发⽣错误
EPOLLHUP 表示连接被挂起
EPOLLET 将触发⽅式设置为边缘触发,系统默认为⽔平触发
EPOLLONESHOT 表示该事件只处理⼀次,下次需要处理时需重新加⼊epoll

假如现在我们将监听文件描述符添加到内核事件表中:

struct epoll_event event;
event.events=EPOLLIN;//监听读事件
event.data.fd=listenfd;//被监听的文件描述符
int res=epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&event);//将event结构体中存储的事件类型添加到内核事件表中。

【3. 开始监听内核事件表的事件】

int epoll_wait(int epfd,struct epoll_event events[],int maxevents,int timeout)
              // 成功返回就绪个数,失败-1,超时0

参数:

  • epfd:内核事件表;
  • events[]:events的数据是 由内核在epoll_wait返回时填充的,有事件就绪的文件描述符和就绪的事件类型;
  • maxevents:数组的长度,表示一次epoll_wait最多返回多少个就绪的文件描述符。
  • timeout:监听时间。

收集 epoll 监控的事件中已经发⽣的事件,如果 epoll 中没有任何⼀个事件发⽣,则最多等待timeout 毫秒后返回。
返回:表示当前发⽣的事件个数
返回0表示本次没有事件发⽣;
返回-1表示出现错误,需要检查errno错误码判断错误类型。

epoll的特点:

【1. 速度快:】

  • select中为fd_set,poll 为 struct pollfd fds[],它们都是用户创建的,用户态存在,每调用一次select或者poll,都会存在两次用户态->内核,内核->用户的数据拷贝,一次是在调用时,一次是在返回时。这样数据结构越大,则越慢。
  • epoll直接就在内核中记录,将其都记录在内核事件表上,在监听时不需要进行拷贝,所以速度比他俩快.

【 2. 查找时间复杂度低:】

  • select,poll返回就绪事件的个数,但不知道在哪,所以要轮询找,每次用户检索就绪事件的时间复杂度为O(N)。
  • epoll直接返回就绪事件链表,链表中有值就有就绪事件,所以每次都是就绪的不用找,每次用户检索就绪事件的时间复杂度为O(1)。

【3. 采取回调函数方式处理就绪事件】

  • select,poll采用轮询方式检测是否有事件发生,循环检测是否有事件就绪,直到找不到。适合关注的文件描述每次都有很大的机率就绪,1000个有900多个就绪,那么就很适合,但是1000个只有一个事件发生,还是要进行轮询1000多次,函数栈帧空间频繁,影响效果;
  • epoll的内核采用的是回调的方式检测文件描述符是否有事件发生。假设1000个,把文件描述符添加到事件表上,文件描述符在红黑树上挂载,除了值,事件类型,还带有一个回调函数,如果文件描述符有事件发生,它就调用回调函数,回调函数的作用是有事件就绪时就把文件描述符添加到双向链表中,返回时把链表的值拷贝到用户态。1000个一个事件发生只会触发一次回调,不用多次循环。适合很多文件描述符,就绪事件描述符少。

用epoll实现tcp服务器的并发

因为epoll封装了很多函数,所以操作起来比起selet和poll代码简洁了很多

服务器代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
//网络头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#define MAX 10//定义最大连接数为10个

int InitSocket()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1) return -1;
    struct sockaddr_in ser;//指明地址信息,是一种通用的套接字地址
    memset(&ser,0,sizeof(ser));
    ser.sin_family = AF_INET;//设置地址家族
    ser.sin_port = htons(6000);//设置端口
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");//设置地址
    int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));//绑定端口号和地址
    if(res == -1)   return -1;
    res = listen(sockfd,5);
    if(res == -1) return -1;
    return sockfd;
}
void epoll_add(int epfd,int fd)
{
    struct epoll_event ev;
    ev.events=EPOLLIN;//读
    ev.data.fd=fd;
    if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
    {
        printf("epoll add failed\n");
    }
}
void epoll_del(int epfd,int fd)
{
    if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
    {
        printf("epoll del failed\n");
    }

}
void accept_client(int epfd,int sockfd)
{
    struct sockaddr_in caddr;
    int len=sizeof(caddr);
    int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
    if(c<0)
    {
        return ;
    }
    printf("accpet c=%d ip=%s\n",c,inet_ntoa(caddr.sin_addr));
    epoll_add(epfd,c);
}
void recv_data(int epfd,int c)
{
    char buff[128]={0};
    int num=recv(c,buff,127,0);
    if(num<=0)//如果num==0说明客户端结束了描述符号
    {
        epoll_del(epfd,c);//移除改客户端对应的描述符
        close(c);
        printf("client close\n");
        
        return ;
    }
    printf("buff (%d)=%s\n",c,buff);
    send(c,"ok",2,0);

}

 int main()
{
    int sockfd = InitSocket();//监听套接字,有客户端链接时就会触发读事件。
    assert(sockfd != -1);
    //创建内核事件表
    int epfd=epoll_create(MAX);//底层,红黑树
    if(epfd==-1)
    {
        exit(1);
    }
    epoll_add(epfd,sockfd);//将监听套接子添加到内核事件表
    struct epoll_event evs[MAX];//用来接收就绪的文件描述符
    while(1)
    {
        int n=epoll_wait(epfd,evs,MAX,5000);
        if(n==-1)
        {
            printf("err\n");
        }
        else if(n==0)
        {
            printf("time out\n");
        }
        else
        {//前n个元素是数据就绪的
            for(int i=0;i<n;++i)
            {
                int fd=evs[i].data.fd;
                if(evs[i].events&EPOLLIN)//看读事件是不是就绪
                {
                    if(fd==sockfd)
                    {
                        accept_client(epfd,sockfd);
                    }
                    else
                    {
                        recv_data(epfd,fd);

                    }

                }
                //if(evs[i].events&EPOLLOUT)
            }

        }
    }
    close(sockfd);
}

客户端的代码都是一样的,这里我就不粘了

ヾ(◍°∇°◍)ノ゙

相关文章:

  • 图像类找工作面试题(二)——常见问题大总结
  • 【文章阅读】Frustratingly Simple Few-Shot Object Detection
  • 网络安全实战:记一次比较完整的靶机渗透
  • 双非本23秋招之路-从考研跑路到某安全大厂(无实习、项目)
  • 影响深度卷积神经网络算法的关键参数是网络结构
  • 微信小程序|使用小程序制作一个核酸检测点查询工具
  • Python3,5行代码,制作Gif动图,太简单了。
  • 【Python】字符串(简介)
  • (附源码)计算机毕业设计SSM基于健身房管理系统
  • 沉睡者IT - 十月之后「牛市」还是「熊市」
  • RK3566快速上手 | ROC-RK3566-PC开发板快速上手
  • MySQL备份测试
  • Redis学习(四)——主从复制、哨兵模式、缓存击穿、穿透、雪崩
  • C++语法——详细剖析类成员函数在内存中存储形式(包括静态)
  • 解决gitlab太占内存问题
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • IP路由与转发
  • java8 Stream Pipelines 浅析
  • java取消线程实例
  • macOS 中 shell 创建文件夹及文件并 VS Code 打开
  • node.js
  • PaddlePaddle-GitHub的正确打开姿势
  • react-core-image-upload 一款轻量级图片上传裁剪插件
  • windows下使用nginx调试简介
  • 闭包,sync使用细节
  • 彻底搞懂浏览器Event-loop
  • 从tcpdump抓包看TCP/IP协议
  • 类orAPI - 收藏集 - 掘金
  • 理解IaaS, PaaS, SaaS等云模型 (Cloud Models)
  • 山寨一个 Promise
  • 手机端车牌号码键盘的vue组件
  • 我建了一个叫Hello World的项目
  • 因为阿里,他们成了“杭漂”
  • PostgreSQL之连接数修改
  • TPG领衔财团投资轻奢珠宝品牌APM Monaco
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • ​io --- 处理流的核心工具​
  • ​LeetCode解法汇总2670. 找出不同元素数目差数组
  • ​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​
  • # 手柄编程_北通阿修罗3动手评:一款兼具功能、操控性的电竞手柄
  • #Java第九次作业--输入输出流和文件操作
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (06)金属布线——为半导体注入生命的连接
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (6)STL算法之转换
  • (C#)if (this == null)?你在逗我,this 怎么可能为 null!用 IL 编译和反编译看穿一切
  • (C语言)fread与fwrite详解
  • (java)关于Thread的挂起和恢复
  • (windows2012共享文件夹和防火墙设置
  • (二十四)Flask之flask-session组件
  • (翻译)Entity Framework技巧系列之七 - Tip 26 – 28
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (机器学习-深度学习快速入门)第一章第一节:Python环境和数据分析
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试