linux学习之旅(32)---I/O 多路复用(epoll)

epoll的产生

上一篇文章中我们讲解了多路I/O复用中的select、poll,它们存在的主要问题有两个:

(1)线程不安全

(2)函数不能告诉应用程序具体需要处理那一路I/O,需要应用程序轮询判断。

基于select和poll存在的这些问题,于是在poll出现的5年之后,也就是2002年,大神Davide Libenzi实现了epoll。epoll是I/O多路复用的最新一个实现,epoll修复了poll和select的绝大部分问题,比如:

(1)epoll是线程安全的。

(2)epoll不仅可以告诉应用程序sock组中数据,还可以告诉应用程序具体是那个sock有数据,不需要程序自己判断。

多路I/O复用----epoll

epoll是linux特有的I/O复用函数,它在实现和使用上与select、poll有很大差异。通过上一篇文章我们直到select和poll都是一个函数,而epoll则是一组函数。epoll将用户关心的文件描述符上的事件放在了一个内核事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集后事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符通过epoll_create来创建。

相关函数:

1、epoll_create

#include <sys/epoll.h>
//创建内核事件表
int epoll_create(int size); 

参数说明:

(1)size:size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。

返回值:函数返回值为事件表的文件描述符。

2、epoll_ctl

//操作内核事件表
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

参数说明:

(1)epfd:内核事件表的文件描述符。

(2)op:指定操作类型。操作类型有以下3种:

EPOLL_CTL_ADD  //向事件表中注册fd上事件
EPOLL_CTL_MOD  //修改fd上注册的事件
EPOLL_CTL_DEL  //删除fd上注册的事件

(3)fd:要操作的文件描述符。

(4)event:作用是指定事件。epoll_event的定义如下:

struct epoll_event
{
    _uint32 events;     //epoll事件
    epoll_data_t data;  //用户数据
};

其中events成员描述事件类型。poll支持的事件类型epoll基本支持的,使用时只需要将poll事件中宏的前缀“POLL”换成“EPOLL”就可以了。除了这些事件类型之外,epoll还额外支持EPOLLET和EPOLLONSHOT两个事件类型。这两个事件时epoll高效运行的关键。

data成员用于存储用户数据,epoll_data_t的定义如下:

typedef union epoll_data
{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

epoll_data_t是一个联合体,其中使用最多的时fd,它可以指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据,但同时只能使用一个成员,所有如果要将文件描述符和用户数据关联起来,以实现快速访问,就必须使用其他的方法,在用户数据中包含fd,这样就可以不适用fd成员。

返回值:epoll_ctl成功返回0;失败返回-1,并设置errno。

3、epoll_wait

#include <sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event* evnts,int maxevents,int timeout);

参数说明:

(1)epfd:内核事件表的文件描述符。

(2)events:指定事件。

(3)maxevents:指定最多监听多少事件。

(4)timeout:和poll函数的timeout参数意义相同。

返回值:和poll的返回值意义相同。

Epoll_wait函数如果检测到事件,就将所有就绪事件从内核事件表(由epfd参数指定)中复制到第二个参数events指向的数组中。这个数组只作用于epoll_wait检测到的就绪事件,所有在判断数据的来源就很方便。

LT和ET模式:

epoll对文件描述符的操作由两种模式:LT(Level Trigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式。那这两种工作模式有什么区别那?

LT(电平触发)模式:epoll默认的工作模式,这种模式下的epoll相当于一个效率较高的poll。对于工作在LT工作模式下的文件描述符,当epoll_wait检测到其上有事件发生并通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,直到该事件被处理。

ET(边沿触发)模式:当向epoll内核事件表中注册一个EPOLLET事件时,epoll将以ET模式操作应用程序,ET模式是一种高效的工作模式。对于工作在ET模式下的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知给应用程序,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知此事件。ET模式在一定程度上大大降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。需要注意的是在ET模式下的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作将会因为没有后续事件而一直处于阻塞状态(饥渴状态)。

EPOLLONESHOT事件

即使使用ET模式,一个socket上的某些事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程(或进程,下同)在读取完某个socket上的数据后开始处理这些程序,而在数据处理的过程中该socket上又有新的数据可读(EPOLLIN再次被触发),此时就会出现两个线程同时处理一个socket,这样就会产生一定的问题。这可以利用epoll的EPOLLONESHOT来解决。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符的EOPLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程不能再操作该socket。再处理完数据后,将EPOLLONESHOT重置,这样就可以让其他线程再次使用(和锁的效果差不多)。

简单epoll服务器程序:

#include <time.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include "sockErrHand.h"

#define PORT 5500
#define MAXLISTEN 32
#define MAXSIZE 512

int main()
{
    int serSock=Socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in serAddr;
    bzero(&serAddr,sizeof(serAddr));
    serAddr.sin_family=AF_INET;
    serAddr.sin_port=htons(PORT);
    serAddr.sin_addr.s_addr=htonl(INADDR_ANY);
    Bind(serSock,(struct sockaddr*)(&serAddr),sizeof(serAddr));
    int eventList=epoll_create(MAXLISTEN);
    struct epoll_event cliEvent;
    struct epoll_event cliEvents[MAXLISTEN];
    cliEvent.events=EPOLLIN;
    cliEvent.data.fd=serSock;
    if(epoll_ctl(eventList,EPOLL_CTL_ADD,serSock,&cliEvent)<0)
    {
        Exit("epoll_ctl");
    }
    Listen(serSock,MAXLISTEN);
    printf("SERVER LISTEN\n");
    while(1)
    {
        //epoll_wait成功返回准备好的文件描述符数目
        int nReady=epoll_wait(eventList,cliEvents,MAXLISTEN,-1);
        if(nReady<0)
        {
            Exit("epoll_wait");
        }
        int i=0;
        for(i=0;i<nReady;++i)
        {
            //有客户端连接
            if(cliEvents[i].data.fd==serSock)
            {
                struct sockaddr_in cliAddr;
                bzero(&cliAddr,sizeof(cliAddr));
                socklen_t cliAddrLen=sizeof(cliAddr);
                int cliSock=Accept(serSock,(struct sockaddr*)(&cliAddr),&cliAddrLen);
                cliEvent.events=EPOLLIN;
                cliEvent.data.fd=cliSock;
                epoll_ctl(eventList,EPOLL_CTL_ADD,cliSock,&cliEvent);
            }
            else
            {
                //处理客户数据
                int i=0;
                char dataBuf[MAXSIZE];
                memset((void*)dataBuf,0,MAXSIZE);
                Read(cliEvents[i].data.fd,(void*)dataBuf,MAXSIZE);
                if(strcmp(dataBuf,"Time\n")==0)
                {
                    memset((void*)dataBuf,0,MAXSIZE);
                    time_t t;
                    time(&t);
                    sprintf(dataBuf,"进程号:%d\n当前服务器时间为:%s\n",getpid(),ctime(&t));
                }
                else
                {
                    sprintf(dataBuf,"%s无效命令\n",dataBuf);
                }
                Write(cliEvents[i].data.fd,(void*)dataBuf,strlen(dataBuf));     
            } 
        }
    }
    Close(serSock);
    return 0;
}

运行结构展示:

浅谈epoll:

再使用epoll开发服务器时,就需要使用上面介绍的三个函数。

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd,int op,struct epoll_event *event);
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

epoll_create的作用:创建一个epoll对象(内核事件表),参数size内核保证能够正确处理的最大句柄数(socket),多于最大数时内核不能保证效果。

epoll_ctl:可以操作上面epoll(内核事件表),添加/修改/删除。

epoll_wait:在规定的事件内,当监视的所有句柄(socket)有事件发生时,就返回用户态的继承。

为什么epoll的效率比select和poll的效率高?

(1)epoll的实现利用了红黑树。在我们调用epoll_create函数之后,内核就会创建一棵红黑树用来存储之后epoll_ctl传来的socket。

(2)epoll的在内部还建立了一张“就绪socket”的链表,用于存储准备就绪的事件。

当函数返回时,直接返回“就绪socket”的链表即可。

这样利用一棵红黑树和一张“就绪socket”链表就可以解决大并发下的socket处理问题。

从这里我们也可以看出epoll的比较使用的情况是:虽然服务器上的连接很多,但同时活跃的用户很少的情况,一旦活跃的用户很多,那么epoll的效率的就会退化的poll差不多(活跃用户多就会成为轮询模型,因为那个用户基本都要处理)。

参考文章:

epoll 的实现原理以及与poll,select 的对比

EPOLL内核原理极简图文解读

epoll详细工作原理

select、poll和epoll的区别:

系统调用

select

poll

epoll

 

 

 

事件集合

用户通过3个参数分别传输需要检测的事件(可读、可写、异常),内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数

统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd,events传入需要检测的事件,内核通过修改pollfd,revents反馈其就绪事件,所以不需要在重新传入events

内核通过一个事件表直接管理用户需要检测的事件,因此每次调用epoll_wait时,无需反复传入用户需要检测的事件。

返回事件集

全部事件集

全部事件集

就绪事件集

应用程序索引就绪文件描述符的事件复杂读

O(n)轮询

O(n)轮询

O(1)

最大支持

文件描述数

1024

65535(端口号最大值)

65535

工作模式

LT(电平触发)

LT

LT和ET(边沿触发)

内核实现

和工作效率

采用轮询方式来检测就绪事件,算法事件复杂度为O(n)

采用轮询方式来检测就绪事件,算法事件复杂度为O(n)

采用回调方式来检测就绪事件,算法事件复杂度为O(1)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值