APUE编程:25---高级I/O之(IO多路复用:epoll()函数)

一、epoll简介

  • 概念:epoll是Linux特有的I/O复用函数。它在实现和使用上与select、 poll有很大差异
  • 如何使用:首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集

二、epoll_create函数

#include <sys/epoll.h>
int epoll_create(int size);
  • 功能:调用epoll_create方法创建一个epoll的句柄,该句柄代表着一个事件表
  • 参数:size参数现在并不起作用,它只是给内核一个提示,告诉内核事件表需要多大(具体有多少个事件,还是依靠后面的epoll_wait参数来处理)
  • 返回值:
    • 成功:返回epoll句柄,它会占用一个fd值(使用完也需要关闭)
    • 失败:返回-1并设置errno值

三、struct  epoll_event结构体

struct epoll_event 
{
    __uint32_t   events;      /* Epoll事件 */
    epoll_data_t data;        /* 用户数据 */
};
  •  功能:epoll_create创建的事件表中的每一个事件就是用此结构体表示的

events成员:

  • 功能:用来描述此个事件的类型、事件类型与poll函数的基本相同,不过是在poll的类型前面加上“E”,但epoll有两个额外的事件类型——EPOLLET 和 EPOLLONESHOT。它们对于epoll的高效运作非常关键。类型如下:
    • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    • EPOLLOUT:表示对应的文件描述符可以写;
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    • EPOLLERR:表示对应的文件描述符发生错误;
    • EPOLLHUP:表示对应的文件描述符被挂断;
    • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后就把该事件移出epoll事件池,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

data成员

typedef union epoll_data 
{
    void  *ptr;
    int  fd;
    __uint32_t  u32;
    __uint64_t  u64;
} epoll_data_t;
  • 功能:data成员用来存储用户数据,data是一个联合体
  • fd:它指定事件所从属的目标文件描述符(使用等最多)
  • ptr成员:可用来指定与fd相关的用户数据
  • 但由于epoll_data_t 是一个联合体,我们不能同时使用其 ptr 成员和 fd 成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t 的fd成员,而在ptr指向的用户数据中包含fd

四、epoll_ctl函数

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能:此函数可以用来向epoll_create创建的事件表中增加、删除、修改事件
  • 返回值:
    • 成功:时返回0
    • 失败:返回-1并设置errno
  • 参数:
    • epfd:要操作的事件表句柄
    • op:指定操作类型
      • EPOLL_CTL_ADD(向事件表中注册fd上的事件)
      • EPOLL_CTL_MOD(修改fd上的注册事件)
      • EPOLL_CTL_DEL(删除fd上的注册事件)
    • fd:要操作的文件描述符
    • event:此参数指定事件,它是epoll_event结构指针类型

五、epoll_wait函数

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
  • 功能:事件表创建成功并且加入事件之后,此函数用来在一段超时时间内等待一组文件描述符上的事件
  • 参数:
    • epfd:等待处理的事件表
    • events:需要自己申请struct  epoll_event数组,此数组用来存放事件表中就绪的事件
    • maxevents:指定最多监听多少个事件,它必须大于0
    • timeout:等待的时间(单位毫秒,与poll接口相同)
  • 返回值:
    • 0:超过了timeout等待的时间,并且没有事件就绪
    • -1:epoll_wait函数出错,同时设置errno值
    • 大于0:事件表中已经准备就绪的事件个数

相对于poll改进的地方

  • epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中,这个数组只用于输出epoll_wait检测到的就绪事件。而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件,这就极大地提高了应用程序索引就绪文件描述符的效
  • 下面是epoll对于epoll改进的代码示意图

六、epoll与poll、select的比较

select模型

  • 最大并发数限制:由于一个进程所打开的fd(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此,select模型的最大并发数就被限制了。
  • 效率问题:每次进行select调用都会线性扫描全部的fd集合。这样,效率就会呈现线性下降。
  • 内核/用户空间内存拷贝问题:select在解决将fd消息传递给用户空间时采用了内存拷贝的方式。这样,其处理效率不高

poll模型

  • 对于poll模型,其虽然解决了select最大并发数的限制,但依然没有解决掉select的效率问题和内存拷贝问题

epoll做了以下的改进

支持一个进程打开较大数目的文件描述符(fd)

select模型对一个进程所打开的文件描述符是有一定限制的,其由FD_SETSIZE设置,默认为1024/2048。这对于那些需要支持上万连接数目的高并发服务器来说显然太少了,这个时候,可以选择两种方案:一是可以选择修改FD_SETSIZE宏然后重新编译内核,不过这样做也会带来网络效率的下降;二是可以选择多进程的解决方案(传统的Apache方案),不过虽然Linux中创建线程的代价比较小,但仍然是不可忽视的,加上进程间数据同步远不及线程间同步的高效,所以也不是一种完美的方案。

但是,epoll则没有对描述符数目的限制,它所支持的文件描述符上限是整个系统最大可以打开的文件数目,例如,在1GB内存的机器上,这个限制大概为10万左右

IO效率不会随文件描述符(fd)的增加而线性下降

传统的select/poll的一个致命弱点就是当你拥有一个很大的socket集合时,不过任一时间只有部分socket是活跃的,select/poll每次调用都会线性扫描整个socket集合,这将导致IO处理效率呈现线性下降。

但是,epoll不存在这个问题,它只会对活跃的socket进行操作,这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的。因此,只有活跃的socket才会主动去调用callback函数,其他idle状态socket则不会。在这一点上,epoll实现了一个伪AIO,其内部推动力在内核。

在一些benchmark中,如果所有的socket基本上都是活跃的,如高速LAN环境,epoll并不比select/poll效率高,相反,过多使用epoll_ctl,其效率反而还有稍微下降。但是,一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了

使用mmap加速内核与用户空间的消息传递无论是select,poll还是epoll,它们都需要内核把fd消息通知给用户空间。因此,如何避免不必要的内存拷贝就很重要了。对于该问题,epoll通过内核与用户空间mmap同一块内存来实现。

内核微调

这一点其实不算epoll的优点了,而是整个Linux平台的优点,Linux赋予开发者微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么,可以在运行期间动态调整这个内存池大小(skb_head_pool)来提高性能,该参数可以通过使用echo xxxx > /proc/sys/net/core/hot_list_length来完成。再如,可以尝试使用最新的NAPI网卡驱动架构来处理数据包数量巨大但数据包本身很小的特殊场景。

七、LT和ET模式

epoll对文件描述符的操作有两种模式

  • ①LT (Level Trigger,水平触发)模式
    • LT模式是epoll的默认的工作模式,这种模式下epoll 相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符,ET模式是epoll的高效工作模式。对于采用LT工作模式的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait会再次向应用程序通告此事件,直到该事件被处理。
  • ②ET (Edge Trigger,边缘触发)模式
    • 对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait用将不再向应用程序通知这一事件,可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高
  • 可以用下面一张图理解:
    • 0代表无事件,1代表有事件。当epoll监听的套接字从无事件变为有事件就代表1次触发
    • 边缘触发:事件只触发一次,在后面的时间线就消失了
    • 水平触发:事件触发之后,如果不处理那么该事件依然存在,随着时间线往后延长,直至你处理完为止

  • 一些相关使用场景:
    • 大数据处理:因为大数据的数据量比较多,因此一次可能处理不完,可以使用水平触发,来多次处理数据
    • 小数据处理:小数据调用边缘触发即可,一次处理完就行
    • 服务器的监听套接字:使用水平触发。当有客户端连接时如果这次不处理,可以放到下一次来处理。但是如果使用边缘触发,本次不处理,下次再处理就消失了,从而失去了这个客户端的连接

、EPOLLONESHOT事件

  • 何时使用EPOLLONESHOT事件:即使我们使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题:比如一个线程(或进程)在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程(或进程)被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。这当然不是我们期望的,我们期望的是一个socket连接在任一时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现
  • EPOLLONESHOT事件的作用:对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其注册的一个可读、可写或者异常事件,且只触发一次,在触发的时候,除非我们重新将这个文件描述符封装为一个事件再次放入epoll事件池。这样,当一个线程在处理某个socket的时候,其他线程是不可能有机会操作该socket的
  • 因此,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置(epoll_ctl(,EPOLL_CTL_MOD,,))这个socket的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket
  • 使用EPOLLONESHOT事件的演示案例:https://blog.csdn.net/qq_41453285/article/details/103168761

九、使用案例

#include<stdio.h>
#include<stdlib.h>
#include<strings.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>

int createSocket(char *ip,char *port);
int set_noblock(int fd);

int main(int argc,char *argv[])
{
    if(argc!=3){
        perror("please enter [IP] [PORT]");
        exit(1);
    }

    int sockFd,epollFd;
    sockFd=createSocket(argv[1],argv[2]);

    //创建一个epoll事件表
    epollFd=epoll_create(256);
    if(epollFd<0){
        perror("epoll_create");
        exit(7);
    }

    //创建一个事件,服务端监听socket作为事件的fd
    struct epoll_event ep_ev;
    ep_ev.events=EPOLLIN;
    ep_ev.data.fd=sockFd;

    
    if(epoll_ctl(epollFd,EPOLL_CTL_ADD,sockFd,&ep_ev)<0){
        perror("epoll_ctl");
        exit(8);
    }

    struct epoll_event ready_ev[128];
    int maxnum=128;
    int timeout=1000;
    int ret;
    
    while(1)
    {
        switch(ret=epoll_wait(epollFd,ready_ev,maxnum,timeout))
        {
            case -1:
                perror("epoll_wait");
                break;
            case 0:
                printf("timeout\n");
                break;
            default:
            {
                int i;
                for(i=0;i<ret;++i)
                {
                    int fd=ready_ev[i].data.fd;
                    if((fd==sockFd) && (ready_ev[i].events&EPOLLIN)){
                        struct sockaddr_in acceptAddr;
                        socklen_t len=sizeof(acceptAddr);
                        int acceptSock=accept(fd,(struct sockaddr*)&acceptAddr,&len);
                        if(acceptSock<0){
                            perror("accept");
                            continue;
                        }
                        printf("get connect ip:%s,port:%d\n",inet_ntoa(acceptAddr.sin_addr),ntohs(acceptAddr.sin_port));

                        ep_ev.events=EPOLLIN | EPOLLET;
                        ep_ev.data.fd=acceptSock;

                        set_noblock(acceptSock);
                        
                        if(epoll_ctl(epollFd,EPOLL_CTL_ADD,acceptSock,&ep_ev)<0){
                            perror("epoll_ctl");
                            close(acceptSock);
                            continue;
                        }
                    }
                    else{
                        if(ready_ev[i].events & EPOLLIN){
                            char buff[1024];
                            ssize_t _s=recv(fd,buff,sizeof(buff),0);
                            if(_s<0){
                                perror("recv");
                                continue;
                            }else if(_s==0){
                                printf("    client close...\n");

                                if(epoll_ctl(epollFd,EPOLL_CTL_DEL,fd,NULL)<0){
                                    perror("epoll_ctl");
                                }
                                close(fd);
                                continue;
                            }else{
                                printf("client:%s",buff);
                                fflush(stdout);
                                
                                ep_ev.events=EPOLLOUT|EPOLLET;
                                ep_ev.data.fd=fd;

                                if(epoll_ctl(epollFd,EPOLL_CTL_MOD,fd,&ep_ev)<0){
                                    perror("epoll_ctl");
                                    continue;
                                }
                            }
                            
                        }else if(ready_ev[i].events & EPOLLOUT){
                            char *msg="I am server";
                            send(fd,msg,strlen(msg),0);
                            if(epoll_ctl(epollFd,EPOLL_CTL_DEL,fd,NULL)<0){
                                perror("epoll_ctl");
                                continue;
                            }
                            close(fd);
                        }
                    }
                }
            }
            break;
        }
    }
    close(sockFd);
    return 0;
}

int createSocket(char *ip,char *port)
{
    int socketFd=socket(AF_INET,SOCK_STREAM,0);
    if(socketFd<0){
        exit(2);
    }

    printf("create socket success\n");
    int opt=1;
    if(setsockopt(socketFd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))<0){
        perror("setsockopt");
        exit(3);
    }
    
    struct sockaddr_in localAddr;
    bzero(&localAddr,sizeof(localAddr));
    localAddr.sin_family=AF_INET;
    localAddr.sin_port=htons(atoi(port));
    if(inet_aton(ip,(struct in_addr*)&localAddr.sin_addr)<=0){
        perror("inet_aton");
        exit(4);
    }

    if(bind(socketFd,(struct sockaddr*)&localAddr,sizeof(localAddr))<0){
        perror("bind");
        exit(5);
    }
    printf("bind success\n");
    
    if(listen(socketFd,10)<0){
        perror("listen");
        exit(6);
    }
    printf("liste success\n");

    return socketFd;
}

int set_noblock(int fd)
{
    int fl=fcntl(fd,F_GETFL);
    return fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值