【源码讲解】sylar服务器框架----IO协程调度模块

epoll以及相关的函数

epoll是用来实现IO多路复用的函数,epoll只要把用户关心的文件描述符上的事件放到内核里的一个事件表中即可,不用像select或者poll传入传出一个存有文件描述符的数组。epoll底层使用一个红黑树和一个双向链表实现。向红黑树中加入或删除需要监听的节点,当存在可读或可写的事件的时候,向链表添加就绪的socket。

epoll_create函数

使用epoll的时候,需要使用一个额外的文件描述符来唯一标识内核中的事件表。使用epoll_wait函数创建这个文件描述符

#include
int epoll_create(int size);

size参数的作用是告诉内核需要多大的事件表,size的大小最好跟最多监听的文件描述符数量差不读。

epoll_ctl函数

使用epoll_ctl函数来操作epoll的内核事件表,它可以向内核事件表添加修改删除fd上的事件。

#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

这个函数共有四个岑书,第一个参数是epoll对象本身的文件描述符,第二个参数是指定的操作类型。第三个参数是要操作的文件描述符,第四个参数用来指定操作的事件。

op有三种,EPOLL_CTL_ADD和EPOLL_CTL_DEL和EPOLL_CTL_MOD,分别用于添加,删除和修改。只能同时存在一个事件。

epoll_event是一个结构体,定义如下(这里抄自epoll源码):

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

events用于描述事件类型。用于告诉epoll监听fd上的哪些事件,它是一系列的事件按位或,可以同时存在多个事件。epoll全部事件类型如下(这里也是抄自epoll源码)

enum EPOLL_EVENTS
  {
    EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN//数据可读
    EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
    EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT//数据可写
    EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
    EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
    EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
    EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
    EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
    EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
    EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
    EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
    EPOLLEXCLUSIVE = 1u << 28,
#define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
    EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
    EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
    EPOLLET = 1u << 31
#define EPOLLET EPOLLET//设置epoll_wait为边缘触发模式
  };

epoll_data_t是一个联合体,也就是说只能同时用到一个成员。本服务器框架用到了fd成员(用于通知线程从epoll_wait函数返回)和ptr成员(用于保存FdContext的指针)。

epoll_ctl成功时返回0, 失败时返回-1并且设置errno。

epoll_wait函数

epoll_wait用于阻塞等待内核事件表上的事件,若有事件可读或可写才会返回。这个函数是本模块的核心。

#include
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

共有四个参数,第一个参数的作用同上,第二个参数是一个指针,用于指向一个数组。当epoll_wait检测到事件时,就将所有就绪(当然是在maxevents的范围内的)的事件从内核事件表中复制到它所指向的数组中。这个数组只用于传出检测到的就绪事件,不能既传入又能传出。第三个参数是指定epoll_wait最多监听多少个事件,第四个参数指定最大超时时间,这是定时器模块的核心。

epoll的两种模式

epol对文件描述符操作用两种模式:LT和ET。LT是水平触发模式,是默认的工作模式。ET是边缘触发模式。

当采用LT时,epoll_wait检测到文件描述符上有事件时,并将就绪事件从内核事件表复制到所指的数组中。若不处理该事件,下次epoll_wait还会传出此就绪事件。采用ET时,则只会传出一次,下次调用epoll_wait不会传出此就绪事件。LT可以降低同一个epoll事件被重复触发的次数。

io协程调度器:

io协程调度器模块继承自调度器模块和定时器模块,重写了一些父类的函数,实现其功能。解决了调度器模块在没有任务执行时空耗cpu的问题。当线程没有任务协程可以执行的时候,会执行idle协程,调度器模块的idle协程是采用cpu忙轮询的方式来检测任务队列,本模块采用epoll_wait来检测任务队列,当添加新任务的时,向管道随便写一点数据,epoll_wait便会返回。当阻塞在epoll_wait上的时候不会消耗cpu资源。

epoll除了支持EPOLLIN事件和EPOLLOUT事件,还支持一些其他的事件,这里为了简化将其他事件全部归类到上面这两类事件中。在epoll_event.data.ptr中保存了Fdcontext的指针,这个结构体保存了一个三元组信息,分别是文件描述符,事件类型,回调函数。本模块在epoll_wait中监听全部注册的fd,若fd满足条件则返回。从epoll_event.data.ptr中取得fd上下文信息,执行其回调函数。

socket fd上下文类:

每个socket fd都对应一个FdContext,包括fd的值,fd上的事件,以及fd的读写事件上下文。FdContext中包含两个EventContext(事件上下文类)。fd的每个事件都有一个事件上下文,保存这个事件的回调函数以及执行回调函数的调度器,每个事件上下文都包含三个成员:执行事件回调的调度器,事件回调协程和事件回调函数。每个事件上下文既支持协程也支持回调函数。每个FdContext中保存的两个EventContext分别是读事件上下文和写事件上下文。FdContext中还包含一个fd和一个变量用于保存IO事件的类型。每个FdContext还包含三个成员函数,分别用于获取指定的EventContext,重置EventContext和触发事件(向任务队列中添加调度任务,准备执行)。

构造函数:

构造函数传入三个参数,分别是线程数量,调度器线程是否参与调度以及调度器的名称。在构造函数中,先使用epoll_create创建epoll对象,然后创建一个管道。使用epoll_ctl注册管道的可读时间,用于监听tickle协程。当tickle协程向管道中写入数据的时候,epoll_wait便会退出。管道的读端采用非阻塞方式,配合边缘触发。接着调用contextResize成员函数,初始化存FdContext的容器大小为32,然后调用start成员函数,在start函数中会创建调度线程,然后调度线程执行run函数进行协程调度。

析构函数:

首先调用stop函数,在stop函数中会回收全部调度线程的资源。然后调用close回收管道以及epoll对象的文件描述符的资源,然后回收全部FdContext资源。由于Scheduler的析构函数是虚函数,然后调用Scheduler的析构函数。

idle函数

run函数直接使用Scheduler父类的run函数,当任务队列没有协程用于调度的时候,线程会执行idle协程。在父类中,idle协程只是简单的空耗cpu检测任务队列中是否有协程用于执行。在子类中重写了父类的方法。对于IO协程调度模块,idle状态应该关注两件事,一个是是否有新的调度任务,另一个是在epoll注册的fd是否有可读或可写的事件,如果上面两件事存在一个,就需要退出idle状态。

在idle函数中,先设置一次epoll_wait最多检测256个就绪事件,创建一个epoll_event的数组。然后进入一个while循环,在循环中,首先调用stopping函数,获取下一次最近的定时器的超时时间。然后再使用一个while循环,循环调用epoll_wait函数。epoll_wait函数有两个作用,一个是监听epoll_wait上是否有可读或可写的事件,另外一个是确定是否有定时器到时并且需要执行回调函数。epoll_wait的超时时间设定的是5000ms和刚才查找的最近的超时时间的最小值。这么做的原因是避免因为定时器超时时间太大,一直阻塞。若是由于超时而退出epoll_wait,则会一直循环下去,直到退出。然后调用listExpiredCb函数收集全部已超时的定时器,并且调用schedule成员函数将其加入到任务队列中。然后遍历epoll_event数组,遍历所有发生的事件。在循环中,首先判断是否是由于tickle协程向管道中写入数据而退出循环,如果是的话需要读完并且扔掉管道中的数据即可。然后根据event.data.ptr找到对应的FdContext的指针进行处理。然后判断是否出错或者socket对端关闭,那么就应该触发对应FdContext的全部事件。然后根据epoll_event中的值判断应该触发读事件还是写事件,或者是全部触发。然后剔除已经发生的事件,将剩下的事件重新加入的epoll_wait中。若对应的FdContext的事件已经全部触发,那么就从epoll对象上删除对应的fd。然后调用对应的FdContext的triggerEvent函数,触发事件。然后m_pendingEventCount--,然后idle协程调用结束,线程开始从任务队列中取出任务执行。

addEvent函数

addEvent函数用于向epoll中添加需要监听的事件。参数共传入三个,第一个参数是fd,第二个参数是事件类型(读事件还是写事件),第三个参数是当事件被触发的时候执行的回调函数。若函数为空,则默认把当前协程作为回调执行体。

在函数中,首先判断传入的fd和FdContext容器的大小。若fd大于FdContext容器的大小,那么就把FdContext以1.5倍扩容(模仿vector)。然后取出fd对应的FdContext。然后判断是否添加了相同的事件,若添加了相同的事件则报错。然后调用epoll_ctl将对应事件加入的epoll,并且设置为非阻塞。然后m_pendingEventCount++,然后向fd对应的FdContext赋值。

delEvent函数:

addEvent函数删除epoll中的事件。参数共传入两个,第一个参数是fd,第二个参数是对应的事件。在删除epoll监听的事件后,还需要m_pendingEventCount--,并且重置fd对应的事件上下文。

cancelEvent函数:

cancelEvent函数取消epoll中的事件。参数共传入两个,第一个参数是fd,第二个参数是对应的事件。跟delEvent函数的区别是,cancelEvent函数在删除之前会触发一次fd对应的事件。

cancelAll函数:

cancelAll函数用于取消对应fd中的全部事件。参数只传入一个fd,跟cancelEvent的区别是他会删除事件,然后都触发一次已注册的事件。

tickle函数:

tickle函数用于通知协程该退出epoll_wait函数,具体实现方法是向管道中随便写点数据。

stopping函数:

stopping函数用于判断是否可以停止,同时获取最近的一个定时器的超时时间。

你可能感兴趣的:(服务器,运维)