• libevent源码分析


    这两天没事,看了一下Memcached和libevent的源码,做个小总结。

    1、入门

    1.1、概述
    Libevent是一个用于开发可扩展性网络服务器的基于事件驱动(event-driven)模型的网络库。Libevent有几个显著的亮点:
    (1)事件驱动(event-driven),高性能;
    (2)轻量级,专注于网络,不如 ACE 那么臃肿庞大;
    (3)源代码相当精炼、易读;
    (4)跨平台,支持 Windows、Linux、*BSD和 Mac Os;
    (5)支持多种 I/O多路复用技术, epoll、poll、dev/poll、select 和kqueue 等;
    (6)支持 I/O,定时器和信号等事件;
    (7)注册事件优先级;
     Libevent 已经被广泛的应用,作为底层的网络库;比如 memcached、 Vomi t、 Nylon、 Netchat等等。

    1.2、一个简单示例

    复制代码
     1 int lasttime;
     2 
     3 static void
     4 timeout_cb(int fd, short eventvoid *arg)
     5 {
     6 struct timeval tv;
     7 struct event *timeout = arg;
     8 int newtime = time(NULL);
     9 
    10 //printf("%s: called at %d: %d ", __func__, newtime,
    11 printf("%s: called at %d: %d ""timeout_cb", newtime,
    12         newtime - lasttime);
    13 lasttime = newtime;
    14 
    15 evutil_timerclear(&tv);
    16 tv.tv_sec = 2;
    17 //重新注册event
    18 event_add(timeout, &tv);
    19 }
    20 
    21 int
    22 main (int argc, char **argv)
    23 {
    24 struct event timeout;
    25 struct timeval tv;
    26  
    27 /* Initalize the event library */
    28 //初始化event环境
    29 event_init();
    30 
    31 /* Initalize one event */
    32 //设置事件
    33 evtimer_set(&timeout, timeout_cb, &timeout);
    34 
    35 evutil_timerclear(&tv);
    36 tv.tv_sec = 2;
    37 //注册事件
    38 event_add(&timeout, &tv);
    39 
    40 lasttime = time(NULL);
    41     
    42 //等待,分发,处理事件
    43 event_dispatch();
    44 
    45 return (0);
    46 }
    复制代码

    这是一个简单的基于libevent的定时器程序,运行结果:

     

    用libevent编程非常简单,只需要调用event_init初始化环境,然后调用event_add注册相应的事件,接着调用event_dispatch等待并处理相应的事件即可。
    调用event_add注册事件时,设置其回调函数。Libevent检测到事件发生时,便会调用事件对应的回调用函数,执行相关的业务逻辑。

    1.3、源代码结构
    Libevent 的源代码虽然都在一层文件夹下面,但是其代码分类还是相当清晰的,主要可分为头文件、内部使用的头文件、辅助功能函数、日志、libevent 框架、对系统 I/O 多路复用机制的封装、信号管理、定时事件管理、缓冲区管理、基本数据结构和基于 libevent的两个实用库等几个部分,有些部分可能就是一个源文件。
    (1)头文件
    主要就是 event.h:事件宏定义、接口函数声明,主要结构体 event 的声明;
    (2)内部头文件
    xxx-internal.h:内部数据结构和函数,对外不可见,以达到信息隐藏的目的;
    (3)libevent框架
    event.c:event 整体框架的代码实现;
    (4)对系统 I/O多路复用机制的封装
    epoll.c:对 epoll 的封装;
    select.c:对 select 的封装;
    devpoll.c:对 dev/poll 的封装;
    kqueue.c:对kqueue 的封装;
    (5)定时事件管理
    min-heap.h:其实就是一个以时间作为 key的小根堆结构;
    (6)信号管理
    signal.c:对信号事件的处理;
    (7)辅助功能函数
    evutil.h  和 evutil.c:一些辅助功能函数,包括创建 socket pair和一些时间操作函数:加、减和比较等。
    (8)日志
    log.h和 log.c:log 日志函数
    (9)缓冲区管理
    evbuffer.c 和buffer.c:libevent 对缓冲区的封装;
    (10)基本数据结构
    compatsys 下的两个源文件: queue.h是 libevent 基本数据结构的实现,包括链表,双向链表,队列等;_libevent_time.h:一些用于时间操作的结构体定义、函数和宏定义;
    (11)实用网络库
         http 和evdns:是基于 libevent 实现的http 服务器和异步 dns 查询库;

    2、核心对象
    结构体event和event_base是libevent的两个核心数据结构,前者代表一个事件对象,后者代表整个事件处理框架。
    2.1、event(事件)

    复制代码
    代码
     1 //event.h
     2 struct event {
     3 TAILQ_ENTRY (event) ev_next;          //已注册事件链表
     4 TAILQ_ENTRY (event) ev_active_next;//就绪事件链表
     5 TAILQ_ENTRY (event) ev_signal_next; //signal链表
     6 unsigned int min_heap_idx;    /* for managing timeouts,事件在堆中的下标 */
     7 
     8 struct event_base *ev_base;
     9 
    10 int ev_fd;      //对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号
    11 short ev_events; //event关注的事件类型
    12 short ev_ncalls; //事件就绪执行时,调用 ev_callback 的次数
    13 short *ev_pncalls;    /* Allows deletes in callback */
    14 
    15 struct timeval ev_timeout;  //timout事件的超时值
    16 
    17 int ev_pri;   /* smaller numbers are higher priority,优先级 */
    18 
    19 void (*ev_callback)(intshortvoid *arg); //回调函数
    20 void *ev_arg; //回调函数的参数
    21 
    22 int ev_res;        /* result passed to event callback */
    23 int ev_flags; //event的状态
    24 };
    25 
    复制代码

    Libevent通过event对象将I/O事件、信号事件和定时器事件封装,从而统一处理,这也是libevent的精妙所有。
    各个字段的具体含义:
    (1) ev_events:event关注的事件类型,它可以是以下3种类型:
    I/O事件: EV_WRITE和EV_READ
    定时事件:EV_TIMEOUT
    信号:    EV_SIGNAL
    辅助选项:EV_PERSIST,表明是一个永久事件
    libevent中的定义为:
    #define EV_TIMEOUT    0x01
    #define EV_READ    0x02
    #define EV_WRITE    0x04
    #define EV_SIGNAL    0x08
    #define EV_PERSIST    0x10    /* Persistant event */
    (2)ev_next,ev_active_next 和 ev_signal_next 都是双向链表节点指针;它们是 libevent 对不同事件类型和在不同的时期,对事件的管理时使用到的字段。
    libevent 使用双向链表保存所有注册的 I/O和 Signal 事件,ev_next 就是该I/O事件在链表中的位置;此链表可以称为“已注册事件链表”;
    同样 ev_signal_next 就是 signal 事件在 signal 事件链表中的位置;
    ev_active_next:libevent 将所有的激活事件放入到链表 active list 中,然后遍历 active list 执
    行调度,ev_active_next就指明了 event 在active list 中的位置;
    (3)min_heap_idx 和 ev_timeout,如果是 timeout 事件,它们是 event 在小根堆中的索引和超时值,libevent 使用小根堆来管理定时事件。
    (4)ev_base指向事件框架实例。
    (5)ev_fd,对于 I/O事件,是绑定的文件描述符;对于 signal 事件,是事件对应的信号;
    (6)eb_flags:libevent 用于标记 event信息的字段,表明事件当前的状态,可能的值有:
    #define EVLIST_TIMEOUT   0x01 // event在time堆中
    #define EVLIST_INSERTED 0x02 // event在已注册事件链表中
    #define EVLIST_SIGNAL    0x04 // 未见使用
    #define EVLIST_ACTIVE    0x08 // event在激活链表中
    #define EVLIST_INTERNAL 0x10 // 内部使用标记
    #define EVLIST_INIT      0x80 // event 已被初始化

    2.2、event_base(事件处理框架)

    复制代码
    代码
     1 //evenet_internal.h
     2 struct event_base {
     3 const struct eventop *evsel; //底层具体I/O demultiplex操作函数集
     4 void *evbase;
     5 int event_count;        /* counts number of total events,总的事件数量 */
     6 int event_count_active;    /* counts number of active events,就绪事件数量 */
     7 
     8 int event_gotterm;        /* Set to terminate loop */
     9 int event_break;        /* Set to terminate loop immediately */
    10 
    11 /* active event management */
    12 //就绪事件链表数组
    13 struct event_list **activequeues;
    14 int nactivequeues;//就绪事件队列个数
    15 
    16 /* signal handling info */
    17 struct evsignal_info sig; //用于管理信号
    18 
    19 struct event_list eventqueue; //注册事件队列
    20 struct timeval event_tv;
    21 
    22 struct min_heap timeheap; //管理定时器的小根堆
    23 struct timeval tv_cache; //记录时间缓存
    24 };
    复制代码

    (1)evsel:libevent 支持Linux、Windows等多种平台,也支持epoll、poll、select、kqueue等多种I/O多路复用模型。如果把 event_init、event_add看成高层抽象的统一事件操作接口,则evsel为这些函数在底层具体的I/O demultiplex的对应的操作函数集。eventop为函数指针的集合:

    复制代码
    代码
     1 struct eventop {
     2 const char *name;
     3 void *(*init)(struct event_base *);
     4 int (*add)(void *struct event *);
     5 int (*del)(void *struct event *);
     6 int (*dispatch)(struct event_base *void *struct timeval *);
     7 void (*dealloc)(struct event_base *void *);
     8 /* set if we need to reinitialize the event base */
     9 int need_reinit;
    10 };
    11 
    复制代码

    在初始化函数event_base_new中,libevent将evsel指向全局数组eventops的具体元素:

    代码

    2.3、主要函数
    2.3.1、event_int(初始化libevent实例)
    struct event_base *
    event_init(void)
    初始化事件处理框架实例,内部调用event_base_new。

    event_base_new的主要逻辑:

    复制代码
    代码
     1 struct event_base *
     2 event_base_new(void)
     3 {
     4 
     5 //初始化小根堆
     6 min_heap_ctor(&base->timeheap);
     7 
     8 //初始化注册事件队列
     9 TAILQ_INIT(&base->eventqueue);
    10 
    11 for (i = 0; eventops[i] && !base->evbase; i++) {
    12 //I/O demultiplex机制实例
    13 base->evsel = eventops[i];
    14 
    15 //初始化I/O demultiplex实例(参见win32_init)
    16 base->evbase = base->evsel->init(base);
    17 }
    18 
    19 //分配1个就绪事件队列
    20 event_base_priority_init(base1);
    21 
    22 }
    复制代码

    2.3.2、event_add(注册事件)
    //注册事件
    int
    event_add(struct event *ev, const struct timeval *tv)
    该函数主要将事件ev加入到事件框架event_base的注册事件链表base->eventqueue。

    2.3.3、event_del(删除事件)
    //删除事件
    int
    event_del(struct event *ev)
    该函数主要将事件ev从相应的链表上删除。

    2.3.4、event_set(设置事件)

    复制代码
    代码
    /*设置event对象
    **ev:事件对象
    **fd:事件对应的文件描述符或信号,对于定时器设为-1
    **events:事件类型,比如 EV_READ,EV_PERSIST, EV_WRITE, EV_SIGNAL
    **callback:事件的回调函数
    **arg:回调函数参数
    */
    void
    event_set(
    struct event *ev, int fd, short events,
          
    void (*callback)(intshortvoid *), void *arg)
    复制代码

    在将事件注册事件处理框架之前,应该先调用event_set对事件进行相关设置。


    2.4、libevent对event的管理
    event结构有3个链表结点域和一个小根堆索引,libevent通过3个链表和一个小根堆对I/O事件、signal事件和timer事件进行管理。
    对于I/O事件,通过event_add将其加入event_base的注册事件链表eventqueue ;就绪时会加入event_base的就绪链表activequeues[];
    对于timer事件,event_add将其加入到event_base的小根堆timeheap;
    Signale 事件的管理相对复杂些,event_add将其加入到注册事件链表,同时,event_add内部会调用I/O demultiplex的add函数(对于I/O事件也一样),比如epoll_add。而add函数又会调用evsignal_add将其加入到 evsignal_info的evsigevents[signo]链表(关于signal,后面会详细介绍)。


    3、事件处理框架主循环
    Libevent将I/O事件、signal事件和timer事件用统一的模型进行处理,这是非常精妙的。libevent主循环函数不断检测注册事件,如果有事件发生,则将其放入就绪链表,并调用事件的回调函数,完成业务逻辑处理。
    3.1、event_dispatch
    //事件处理主循环
    int
    event_dispatch(void)
    这是呈现给外部的接口,它的实现很简单,即调用event_loop,而event_loop调用event_base_loop,event_base_loop完成实际的主循环逻辑。

    3.2、event_base_loop
    主要算法:

    复制代码
     1 done = 0;
     2 while (!done) {
     3 
     4 /*如果没有就绪事件,根据timer heap中事件的最小超时时间,计算I/O demultiplex的
     5 **最大等待时间. 相反,如果有就绪事件,则清除tv,即I/O demultiplex不应该等待.
     6 */
     7 if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
     8     timeout_next(base&tv_p);
     9 else {
    10     /* 
    11      * if we have active events, we just poll new events
    12      * without waiting.
    13      */
    14     evutil_timerclear(&tv);
    15 }
    16 
    17 /* If we have no events, we just exit */
    18 //没有事件处理,则退出循环
    19 if (!event_haveevents(base)) {
    20     event_debug(("%s: no events registered.", __func__));
    21     return (1);
    22 }
    23 
    24 //tv_p为I/O demultiplex的超时时间
    25 //处理signal事件和I/O事件
    26 res = evsel->dispatch(base, evbase, tv_p);
    27 
    28 //处理timeout事件,对于超时的事件,将其放到就绪事件链表
    29 timeout_process(base);
    30 
    31 if (base->event_count_active) {
    32     //处理就绪事件
    33     event_process_active(base);
    34     if (!base->event_count_active && (flags & EVLOOP_ONCE))
    35         done = 1;
    36 else if (flags & EVLOOP_NONBLOCK)
    37     done = 1;
    38 
    39 }//end while
    40 
    复制代码

    3.3、timeout_next

    复制代码
     1 /*根据timer heap中事件的最小超时时间,计算I/O demultiplex的最大等待时间.
     2 **为了及时处理timer事件,I/O demultiplex的最大等待时间不应该超过timer事件中最小的超时时间,
     3 **否则,timer事件就不能得到及时处理
     4 */
     5 static int
     6 timeout_next(struct event_base *basestruct timeval **tv_p)
     7 {
     8     struct timeval now;
     9     struct event *ev;
    10     struct timeval *tv = *tv_p;
    11 
    12     //如果没有timer事件,则直接返回
    13     if ((ev = min_heap_top(&base->timeheap)) == NULL) {
    14         /* if no time-based events are active wait for I/O */
    15         *tv_p = NULL;
    16         return (0);
    17     }
    18 
    19     if (gettime(base&now) == -1)
    20         return (-1);
    21 
    22     //如果最小的timer事件已经超时,则清除tv,即I/O demultiplex不应该等待.
    23     if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {
    24         evutil_timerclear(tv);
    25         return (0);
    26     }
    27 
    28     //更新I/O demultiplex可以等待的最大时间:ev->ev_timeout - now
    29     evutil_timersub(&ev->ev_timeout, &now, tv);
    30     return (0);
    31 }
    32 
    复制代码

    3.4、dispatch函数
    调用底层I/O multiplex的dispatch函数,具体的实现可以参见epoll的实现epoll_dispatch。

    3.5、event_process_active

    代码

    4、Timer事件
    Timer事件的处理本身比较简单,不再赘述。

    5、signal事件
    5.1、socket pair
    Libevent通过socketpair,将signal事件与I/O事件完美的统一起来。Socketpair,简单的说就一对socket,一端用于写,一端用于读。工作方式如下:

     为了与I/O事件统一起来,libevent内部使用了一个针对read socket的读事件。

    5.1.1、Socketpair的创建
    与 信号事件的初始化工作都是在evsignal_init中完成的,而evsignal_init通过调用evutil_socketpair创建 socketpair。对于Unix平台,有socketpair系统调用;对于Windows,则相对复杂一些,具体见 evutil_socketpair函数的实现。

    5.2、evsignal_info
    在event_base内部有一个evsignal_info类型的字段sig,它是用于管理signal事件的核心数据结构:

    复制代码
    代码
     1 //evsignal.h
     2 struct evsignal_info {
     3 struct event ev_signal;    //内部socket读事件
     4 int ev_signal_pair[2];  //对应socket pair的两个socket描述符
     5 int ev_signal_added;  //内部socket读事件是否已经加入注册链表
     6 volatile sig_atomic_t evsignal_caught; //是否有信号发生
     7 //信号事件链表数组,evsigevents[signo]表示注册信号signo的事件
     8 struct event_list evsigevents[NSIG]; 
     9 //具体记录每个信号触发的次数,evsigcaught[signo]是记录信号 signo被触发的次数
    10 sig_atomic_t evsigcaught[NSIG];
    11     
    12 //sh_old记录了原来的 signal 处理函数指针,当信号 signo 注册的 event 被清空时,需要重新设置其处理函数
    13 #ifdef HAVE_SIGACTION
    14 struct sigaction **sh_old;
    15 #else
    16 ev_sighandler_t **sh_old;
    17 #endif
    18 int sh_old_max;
    19 };
    20 
    复制代码

    5.3、主要函数
    5.3.1、evsignal_init
    主要完成evsignal_info的初始化,主要算法:

    复制代码
    代码
     1 int
     2 evsignal_init(struct event_base *base)
     3 {
     4 evutil_socketpair(AF_UNIX, SOCK_STREAM, 0base->sig.ev_signal_pair);
     5 base->sig.sh_old = NULL;
     6 base->sig.sh_old_max = 0;
     7 
     8 //事件发生次数设为0
     9 base->sig.evsignal_caught = 0;
    10 memset(&base->sig.evsigcaught, 0sizeof(sig_atomic_t)*NSIG);
    11 /* initialize the queues for all events */
    12 for (i = 0; i < NSIG; ++i)
    13 TAILQ_INIT(&base->sig.evsigevents[i]);
    14 
    15 evutil_make_socket_nonblocking(base->sig.ev_signal_pair[0]); //写端
    16 
    17 //设置内部读事件
    18 event_set(&base->sig.ev_signal, base->sig.ev_signal_pair[1], 
    19 EV_READ | EV_PERSIST, evsignal_cb, &base->sig.ev_signal); //读端
    20 base->sig.ev_signal.ev_base = base;
    21 
    22 //sig.ev_signal == EV_READ | EV_PERSIST | EVLIST_INTERNAL
    23 base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL;
    24 }
    复制代码

    该 函数的关键在于这里会设置libevent用于管理信号事件的内部读事件evsignal_info的ev_signal,并将该事件对应的文件描述符设 为socket pair的读端。该函数由I/O multiplex的init函数调用。注:这里只是设置,而并没有注册socket pair的读事件(见下一节)。

    5.3.2、evsignal_add
    当调用event_add注册信号事件时,内部会先调用 I/O multiplex的add函数,add函数又会调用evsignal_add,将事件加到evsignal_info内部的信号事件链表。然后再 event_queue_insert将其添加到event_base的注册事件链表。

    代码

    这里有两个地方需要注意,一是调用_evsignal_set_handler设置外部注册信号事件对应的信号的信号处理函数evsignal_handler:

    复制代码
    代码
     1 static void
     2 evsignal_handler(int sig)
     3 {
     4     int save_errno = errno;
     5 
     6     //设置信号事件的发生次数
     7     evsignal_base->sig.evsigcaught[sig]++;
     8     evsignal_base->sig.evsignal_caught = 1;
     9 
    10 #ifndef HAVE_SIGACTION
    11     signal(sig, evsignal_handler);
    12 #endif
    13 
    14     /* Wake up our notification mechanism */
    15     //向socket pair的写端写数据
    16     send(evsignal_base->sig.ev_signal_pair[0], "a"10);
    17     errno = save_errno;
    18 }
    复制代码

    当用户注册信号事件对应的信号发生时,OS转到evsignal_handler函数,从而设置sig.evsignal_caught,并向socket pair的写端发送数据。
    二是通过调用event_add完成内部socket pair的读事件sig->ev_signal的注册。最后,将(外部)事件添加到信号事件链表。
    5.3.2、与主循环结合
    信号事件完成了注册,libevent就会在主循环中,等待事件发生,并处理事件。为了理解,来看看具体I/O demultiplex的dispatch函数:

    复制代码
    代码
     1 static int
     2 epoll_dispatch(struct event_base *basevoid *arg, struct timeval *tv)
     3 {
     4     struct epollop *epollop = arg;
     5     struct epoll_event *events = epollop->events;
     6     struct evepoll *evep;
     7     int i, res, timeout = -1;
     8 
     9     if (tv != NULL)
    10         timeout = tv->tv_sec * 1000 + (tv->tv_usec + 999/ 1000;
    11 
    12     if (timeout > MAX_EPOLL_TIMEOUT_MSEC) {
    13         /* Linux kernels can wait forever if the timeout is too big;
    14          * see comment on MAX_EPOLL_TIMEOUT_MSEC. */
    15         timeout = MAX_EPOLL_TIMEOUT_MSEC;
    16     }
    17 
    18     //等待I/O事件
    19     res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
    20 
    21     if (res == -1) {
    22         if (errno != EINTR) {
    23             event_warn("epoll_wait");
    24             return (-1);
    25         }
    26         //epoll_wait被信号中断
    27         evsignal_process(base);
    28         return (0);
    29     } else if (base->sig.evsignal_caught) {//发生了信号事件
    30         //处理信号事件
    31         evsignal_process(base);
    32     }
    33 //
    34 }
    复制代码

    epoll_dispatch函数调用epoll_wait函数等待I/O发生。然后,如果有信号事件发生,则调用evsignal_process处理信号事件,evsignal_process的逻辑比较简单,它只是将事件从注册事件链表转移到就绪事件链表。

    还 记得evsignal_handler函数吗?它是所有(外部)信号事件对应的信号的信号处理函数,将实际的信号发生时,OS会转而执行 evsignal_handler函数,而它便向socket pair的写端写数据,而读端收到数据。而此时,libevent的内部socket pair读事件已经完成注册。libevent正阻塞在epoll_wait处,当socketp pair读端收到数据时,libevent便从epoll_wait处返回。总之,signal事件通过socket pair,与I/O事件实现完美的统一。

    Libevent从epoll_wait返回后,它调用evsignal_process处理信号事件,然后调用event_active将I/O事件(包括内部的socket pair读事件)转移到就绪事件链表。

    Socket pair的读事件回调函数:

    复制代码
    代码
     1 static void
     2 evsignal_cb(int fd, short what, void *arg)
     3 {
     4 static char signals[1];
     5 #ifdef WIN32
     6 SSIZE_T n;
     7 #else
     8 ssize_t n;
     9 #endif
    10 //接收数据
    11 = recv(fd, signals, sizeof(signals), 0);
    12 if (n == -1)
    13 event_err(1"%s: read", __func__);
    14 }
    复制代码

    6、libevent的应用
    Libevent 是一个非常优秀的开源网络库,它被许多其它开源程序使用。Memcache使用libevent作为底层的网络处理组件,并采用主线程(main thread,单一)+工作线程(work thread,多个)的多线程模型(这将在Memcached的分析中详细介绍)。

     

  • 相关阅读:
    Android移动软件开发总结
    (译)综合指南:通过Ubuntu 16.04上从Source构建来安装支持GPU的Caffe2
    caffe2 环境的搭建以及detectron的配置
    SSD-Tensorflow 从工程角度进行配置
    简单实现Ubuntu16.04 + caffe2 + CUDA9.0 + cuDNN8.0
    Tensorflow1.5.0+cuda9.0+cudnn7.0+gtx1080+ubuntu16.04
    Cuda 9.2 CuDnn7.0 官方文档解读
    解决 Faster R-CNN 图片中框不在一张图片上显示的问题
    (转载)YOLO配置文件理解
    CycleGAN 配置及其实现
  • 原文地址:https://www.cnblogs.com/breg/p/3689333.html
Copyright © 2020-2023  润新知