linux高并发多路I/O复用之epoll模型

Epoll模型

概念:

  epoll是Linux内核为处理大批句柄而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著的减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果,而不是迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一个原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select\poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_pwait的调用,提高应用程序的效率。

工作方式:

LT(level triggered):水平触发,缺省方式,同时支持block和no-block socket,在这种做法中,内核告诉我们一个文件描述符是否被就绪了,如果就绪了,你就可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错的可能性较小。传统的select\poll都是这种模型的代表。

ET(edge-triggered):边沿触发,高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪状态时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如:你在发送、接受或者接受请求,或者发送接受的数据少于一定量时导致了一个EWOULDBLOCK错误)。但是请注意,如果一直不对这个fd做IO操作(从而导致它再次变成未就绪状态),内核不会发送更多的通知。

区别:

LT事件不会丢弃,而是只要buffer里面有数据可以让用户读取,则不断的通知你。而ET则只在事件发生之时通知。

使用方式:

使用方式:

  (先进行创建,在进行注册)

1、int epoll_create(int size)

创建一个epoll句柄,参数size用来告诉内核监听的数目。

2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

Epoll_ctl事件注册函数,

  参数epfd为epoll的句柄;

参数op表示动作,用3个宏来表示:

EPOLL_CTL_ADD(注册新的fd到epfd),

EPOLL_CTL_MOD(修改已经注册的fd的监听事件),

EPOLL_CTL_DEL(从epfd删除一个fd);

  参数fd为需要监听的标识符;

  参数event告诉内核需要监听的事件,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的事件的属性,有如下属性:*/  

  epoll_data_t data;  /* User data variable ,用户数据变量*/  

};

以上在epoll_data_t的结构,是一个联合体,通常使用的都是fd直接拷贝的文件描述符号,还有另外的指针参数,除此之外,可以将对应的参数传入其中,待事件发生时,自己处理对应的事件。可以通过传入对应的参数,自己实现对应的处理函数来处理实际情况。

例如,在使用时,可以将sockfd和对应的处理函数一并传入之*ptr变量,等待事件触事,可获取对应的参数,将函数自行处理相关的事件即可。

struct epoll_event myevent;

typedef int (*fun)(int) FUN;

typedef struct myhand{

        FUN f;

        int fd;

}MY_HANED;

void process(){ cout<<"This is epoll using"<<endl;}

MY_HANED myhand;

myhand.f = process;

myhand.fd = sockfd;

myevent.event = EPOLL_ET;

myevent.data.ptr = & myhand;

如上为使用指针变量样例。

  其中events可以用以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

EPOLLOUT:表示对应的文件描述符可以写

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

EPOLLERR:表示对应的文件描述符发生错误

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

  等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

4,使用epoll之所以能高效实现百万级并发的原因如下

        在epoll三个接口函数使用的时候.,当创建epoll结构的时候,内核中创建对应的缓存,内核系统中的cache使用的是红黑树结构,存储对应的sockfd,支持快速的查询,插入,查找操作。分配连续的物理内存页,并且为每个对象分配相应的slab块。在每次使用的时候都是已经分配的空闲的对象。

       epoll的高效就在于内核中在存储epoll_ctl时插入的socket的时候除了分配对应的高速cache之外,还会建立一个list表用来存储就绪事件的链表,在epoll_wait时,只需要查看该list表中是否有对应的数据即可。有数据返回,无数据将继续sleep,即便设置了timeout,超时后也返回。

       除此之外,在epoll中存在两种模式,LT and ET两种模型。这两种的区别(如上图已经描述)。

       其内部的实现的原理:内核中的除了拥有存储接收来的socket的红黑树缓存,还有一个数据就绪的链表,当模式处于LT模式,当事件发生的时候,在epoll_wait后,将就绪的sockfd发送至用户态,从而会清空list就绪时间等待链表表,然而epoll_wait还做了一个检查,即检查socket的模式,如果是LT模式,则会重新将其存放至list中,所以,LT模式发生事件时,用户没有处理完事件,内核会再次告知用户态处理此事件。相反,ET模式,仅仅只支持非阻塞状态,除非当出现新的中断事件,才会告知用户态去处理新的事件,并且只会告知一次。

注:epoll操作监听文件描述符的操作如下总结:

1>调用 epoll_create 时,做了以下事情:
    a>内核帮我们在 epoll 文件系统里建了个 file 结点;
    b>在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket;
    c>建立一个 list 链表,用于存储准备就绪的事件。
2>调用 epoll_ctl 时,做了以下事情:
    a>把 socket 放到 epoll 文件系统里 file 对象对应的红黑树上(已存在立即返回); 
    b>并在内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到
准备就绪 list 链表里。
3>调用 epoll_wait 时,做了以下事情:
    a>观察 list 链表里有没有数据。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即
    使链表没数据也返回。

      

例如:简单实用过程,见附几个文件。

  程序共包含2个头文件和3个cpp文件。其中3个cpp文件中,每一个cpp文件都是一个应用程序,server.cpp:服务器程序,client.cpp:单个客户端程序,tester.cpp:模拟高并发,开启10000个客户端去连服务器。

utils.h头文件,就包含一个设置socket为不阻塞函数,如下:

//该函数自己定义,实现将套接字设置为非阻塞状态

int setnonblocking(int sockfd)

{

    CHK(fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK));

    return 0;

}

注:fcntl函数的使用方法;

所在头文件 #include <fcntl.h>,#include <unistd.h>

int fcntl(int fd, int cmd);

 

int fcntl(int fd, int cmd, long arg);         

 

int fcntl(int fd, int cmd, struct flock *lock);

fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符,针对cmd的值,fcntl能够接受第三个参数(arg)

fcntl函数有5种功能:

     1.复制一个现有的描述符(cmd=F_DUPFD).

 

    2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).

 

    3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).

 

     4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).

 

     5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

epoll实现原理:

一、epoll相关的数据结构

  最重要的两个数据结构是红黑树和就绪链表红黑树用于管理所有的文件描述符fd,就绪链表用于保存有事件发生的文件描述符。

结构:

  当向系统中添加一个fd时,就创建一个epitem结构体。eventpoll用于管理所有的epitem。

在这里插入图片描述

二、epoll_create函数

  epoll_create函数的功能是创建eventpoll。

三、epoll_ctl函数

  epoll_ctl函数的功能是对文件描述符进行增删改查。

四、ep_insert函数

  ep_insert函数的功能是插入一个文件描述符到红黑树上。

五、ep_poll_callback函数

  所以ep_poll_callback函数主要的功能是当被监视文件的等待事件就绪时,将文件描述符对应的epitem实例添加到就绪链表中,导致rdlist不空,进程被唤醒,epoll_wait()得以继续执行,之后内核会将就绪链表中的事件从内核空间拷贝到用户空间。

六、epoll_wait函数

  epoll_wait调用ep_poll,当rdlist(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。然后就将就绪的events和data发送到用户空间(ep_send_events()),如果ep_send_events()返回的事件数为0,并且还有超时时间剩余(jtimeout),那么我们retry,期待不要空手而归。

七、ep_send_events函数

  ep_send_events函数,它扫描txlist中的每个epitem,调用其关联的fd对应的的poll方法,取得fd上较新的events(防止之前events被更新)即revents,之后将revents和相应的data拷贝(__put_user())到用户空间。如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist,如果是ET模式则不再加入rdlist

八、epoll的工作过程详解

  通过上面的源码分析可以看出epoll的工作过程如下:

1. epoll_wait()调用ep_poll(),如果就绪链表rdlist为空,则挂起当前进程,直到rdlist不为空时被唤醒,这个时候会调用ep_send_events()将实际发生的事件reventsdata从内核空间拷贝到用户空间(拷贝调用的是__put_user,并不存在什么共享内存之类的)。

2. 当文件描述符fd的状态改变时(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback() 被调用。

3. ep_poll_callback()将有事件发生的文件描述符(epitem)加入到就绪链表rdlist 中,这时候就绪链表不为空,epoll_wait() 进程被唤醒。

4. ep_send_events()扫描就绪链表,调用每个文件描述符的poll函数返回revents,之后将reventsdata从内核空间拷贝到用户空间。如果是ET模式, epitem是不会再进入到就绪链表,除非fd再次发生了状态改变ep_poll_callback被调用。如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。这样如果下次再次被唤醒就会给用户空间再次返回事件。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值