libevent源码分析之深入信号处理

信号

关于统一事件源, 本文是在对其有所了解的情况下进行的,旨在加深对libevent的整体理解

首先,想要了解信号事件的处理,我们需要知道关于信号这部分的接口、内容分别在哪里
纵观整个文件夹, 可以看到evsignal-internal.h, signal.c, signal-test.c 这几个文档, 接口在哪里呢?
回忆之前所学内容, 首先肯定是找event_base , 在添加信号事件时会用到信号事件的添加, 在IO复用初始化函数中调用过信号事件的初始化

于是在event_base的定义中我们找到了:
struct event_base {
    ... ...

    /** Function pointers used to describe the backend that this event_base
    * uses for signals */
    const struct eventop *evsigsel;
    /** Data to implement the common signal handelr code. */
    struct evsig_info sig;

    ... ...

    /** Mapping from signal numbers to enabled (added) events. */
    struct event_signal_map sigmap;

    ... ...
};



首先, 对于第一个struct eventop *evsigsel, 在IO复用选择的学习中我们知道这是对IO复用一系列操作的抽象结构体, 既然对统一事件源有所了解了,那么我们肯定知道对信号的添加和删除等操作与普通的描述符添加删除等操作是完全不同的, 所以必须另行一套处理函数
//对于信号处理,添加和删除操作就足够了
static const struct eventop evsigops = {
    "signal",
    NULL,
    evsig_add,
    evsig_del,
    NULL,
    NULL,
    0, 0, 0
};

接着, 看struct event_signal_map sigmap, 这个我们肯定有所了解, 这是对应不同信号的处理函数的哈希表, 把所有要处理的信号及其处理函数放在一起等待将来处理

最后, struct evsig_info sig, 这是新内容:


    /*我们知道,统一事件源其实就是利用管道, 将得到的信号通过管道发送给管道的一端, 该端被IO复用所监听, 知道是哪个信号后, 调用该信号注册的处理函数即可再来看这个结构体就明了了*/
struct evsig_info {
    /* Event watching ev_signal_pair[1] */
    /*ev_signal是用来给所有信号在IO复用中注册的, 提供的fd是管道的一端, 一般使用的管道是scoketpair生成的管道,属于全双工管道,所以哪一端传输哪一端接收无关的*/
    struct event ev_signal;
    /* Socketpair used to send notifications from the signal handler */
    /*ev_signal_pair即为管道两端*/
    evutil_socket_t ev_signal_pair[2];
    /* True iff we've added the ev_signal event yet. */
    /*ev_signal_added是用于判断ev_signal是否已经被添加到了IO复用中*/
    int ev_signal_added;
    /* Count of the number of signals we're currently watching. */
    /*ev_n_signals_added表示我们现在在监听几个信号*/
    int ev_n_signals_added;

    /* Array of previous signal handler objects before Libevent started
    * messing with them.  Used to restore old signal handlers. */
    /*接下来则是sigaction的数组, 用于存储该信号之前的处理函数, 因为如果用户选择放弃监听该信号,那么对该信号的处理函数就要恢复之前的系统默认处理方式,如果不做存储,那在用户放弃监听后就不知道如和处理了*/
#ifdef _EVENT_HAVE_SIGACTION
    struct sigaction **sh_old;
#else
    ev_sighandler_t **sh_old;
#endif
    /* Size of sh_old. */
    int sh_old_max;
};


最外层,对所有信号的集中处理

到此为止,我们认识了信号在libevent中的对外接口, 循着evsignal-internal.h文档中对信号处理函数的声明,我们接下来就仔细看看关于信号的操作(在signal.c文档中)
是否还记得在后端IO复用选取的文章中,在解析IO复用的初始化函数时碰到的一行代码:
evsig_init(base); 
虽然没有看过其源代码, 但是根据我们的了解,可以模糊的猜测其用意就是在IO复用中注册自己的事件,且提供的描述符是管道的一端,下面就详细的看看:

int
evsig_init(struct event_base *base)

    /*
     * Our signal handler is going to write to one end of the socket
     * pair to wake up our event loop.  The event loop then scans for
     * signals that got delivered.
     */
        //先来个管道用用
    if (evutil_socketpair(
            AF_UNIX, SOCK_STREAM, 0, base->sig.ev_signal_pair) == -1) {
#ifdef WIN32
        /* Make this nonfatal on win32, where sometimes people
           have localhost firewalled. */
        event_sock_warn(-1, "%s: socketpair", __func__);
#else
        event_sock_err(1, -1, "%s: socketpair", __func__);
#endif
        return -1;
    }

        //设置在调用exec系列函数时此描述符自动关闭,对于系统编程来说这是一个容易忽略的地方,问题说大不大,说小也不小
        //主要是为了防止在子进程中向管道发送数据或其他影响行为
    evutil_make_socket_closeonexec(base->sig.ev_signal_pair[0]);
    evutil_make_socket_closeonexec(base->sig.ev_signal_pair[1]);
        //初始化
    base->sig.sh_old = NULL;
    base->sig.sh_old_max = 0;
        //设置非阻塞
    evutil_make_socket_nonblocking(base->sig.ev_signal_pair[0]);
    evutil_make_socket_nonblocking(base->sig.ev_signal_pair[1]);

        //新建事件,用于将事件放入IO复用中监听
    event_assign(&base->sig.ev_signal, base, base->sig.ev_signal_pair[1],
        EV_READ | EV_PERSIST, evsig_cb, base);

    base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL;
        //设置优先级,关于优先级以后讨论
    event_priority_set(&base->sig.ev_signal, 0);

    base->evsigsel = &evsigops;

    return 0;
}
//整个初始化函数执行完,我们都没有看到event_add之类的函数将此信号事件放到IO复用中去. 仔细想想, 既然是初始化, 那自然是在用户指定监听哪个信号之前进行的, 此时并没有 
//--信号需要监听, 那么又何必提前开始监听信号事件处理函数呢? 那么到底是在什么时候添加这个事件的呢, 下面会看到
//在新建事件时,我们发现注册的事件处理函数名叫 evsig_cb, 拿来看看


static void
evsig_cb(evutil_socket_t fd, short what, void *arg)
{
    static char signals[1024];
    ev_ssize_t n;
    int i;
    int ncaught[NSIG];        //一个int类型的数组,用于捕捉收到的信号
    struct event_base *base;

    base = arg;

    memset(&ncaught, 0, sizeof(ncaught));

    while (1) {
        n = recv(fd, signals, sizeof(signals), 0);            //n表示从管道的一端收到的字节数
        if (n == -1) {
            int err = evutil_socket_geterror(fd);
            if (! EVUTIL_ERR_RW_RETRIABLE(err))            //因为是非阻塞,我们必须保证将所有数据接收完整才能退出循环
                event_sock_err(1, fd, "%s: recv", __func__);
            break;
        } else if (n == 0) {                        
            /* XXX warn? */
            break;
        }
        for (i = 0; i < n; ++i) {
            ev_uint8_t sig = signals[i];
            if (sig < NSIG)
                ncaught[sig]++;                //可以看出,如果接收到某信号,那么对应于数组上的某个位置值就加1
                                    //也可以看出,信号发生几次都详细的记录在案,以后就会调用几次该信号对应的处理事件
        }
    }

    EVBASE_ACQUIRE_LOCK(base, th_base_lock);
    for (i = 0; i < NSIG; ++i) {
        if (ncaught[i])                        //在这里激活事件,此处先领会大意,详细关于事件激活会在以后深入
            evmap_signal_active(base, i, ncaught[i]);
    }
    EVBASE_RELEASE_LOCK(base, th_base_lock);
}


信号添加

OK,现在我们知道了关于信号的一些处理,且我们知道添加信号处理和添加普通的文件描述符是不同的,接下来就看看如何将信号与其对应的处理事件添加进去的

于是我们寻找哪里会使用到这个添加函数  event_add ---> event_add_internal ---> evmap_signal_add
且添加信号事件,那么肯定是添加在其signal map中的:
int
evmap_signal_add(struct event_base *base, int sig, struct event *ev)
{
    const struct eventop *evsel = base->evsigsel;
    struct event_signal_map *map = &base->sigmap;
    struct evmap_signal *ctx = NULL;

    if (sig >= map->nentries) {
        if (evmap_make_space(
            map, sig, sizeof(struct evmap_signal *)) == -1)
            return (-1);
    }

        //下面的这个函数我们之前介绍过,简单的说就是获取该信号对应的TAILQ的头节点,用ctx指向
    GET_SIGNAL_SLOT_AND_CTOR(ctx, map, sig, evmap_signal, evmap_signal_init,
        base->evsigsel->fdinfo_len);

    //既然该信号的事件队列为空,那么就说明,可能其他所有信号的事件都是空的,(注意是可能)这样一来,我们又知道在init函数中并没有将信号事件添加到IO复用中去,于是在这里我们就可能要添加
        //还有一点在于信号本身的处理函数,作为事件源处理方式,我们知道IO复用通过管道一端传来的数据判断是哪个信号. 可管道的另一端是怎样神秘的存在呢?它就是一般在系统编程中的信号处理函数,要注意的是这里的信号处理函数和我们讲的libevent中的信号处理函数并不同。 系统编程中的信号处理函数调用write管道将信号的值(int类型)传给IO复用, IO复用读取管道得到该信号的值,于是再调用libevent中的信号处理函数进行处理
        //之所以这么认为,还因为下面已经有插入操作了,所以这里才如此揣测evsel->add
    if (TAILQ_EMPTY(&ctx->events)) {
        if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
            == -1)
            return (-1);
    }

    TAILQ_INSERT_TAIL(&ctx->events, ev, ev_signal_next);

    return (1);
}


再次回顾信号的抽象操作结构体定义:
static const struct eventop evsigops = {
    "signal",
    NULL,
    evsig_add,
    evsig_del,
    NULL,
    NULL,
    0, 0, 0
};

其中add操作对应的函数即为evsig_add
//是如何调用的:evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
//根据以上的分析,evsig_add函数主要两个目的是: 1、添加信号处理事件的管道一端到IO复用中去   2、为信号设置系统级别的处理函数
static int
evsig_add(struct event_base *base, evutil_socket_t evsignal, short old, short events, void *p)
{
    struct evsig_info *sig = &base->sig;
    (void)p;

    EVUTIL_ASSERT(evsignal >= 0 && evsignal < NSIG);

    /* catch signals if they happen quickly */
    EVSIGBASE_LOCK();
        //下面的判断条件是 1、信号接收的base和当前作为参数传入的base不一致     2、监听的信号数量大于0
        //下面两个是全局变量, 被初始化为NULL 和 0
        //在这之前,我们并未遇到这两个变量,这里是第一次,于是这个判断在第一次添加一个信号的时候不会进入
    if (evsig_base != base && evsig_base_n_signals_added) {
        //下面这段话的意思是, 一个进程只能有一个base可以接收信号, 这个base应该是最近刚添加过信号的或是最近刚调用event_base_loop的那个
        event_warnx("Added a signal to event base %p with signals "
            "already added to event_base %p.  Only one can have "
            "signals at a time with the %s backend.  The base with "
            "the most recently added signal or the most recent "
            "event_base_loop() call gets preference; do "
            "not rely on this behavior in future Libevent versions.",
            base, evsig_base, base->evsel->name);
    }
        //设置全局变量的值
    evsig_base = base;
    evsig_base_n_signals_added = ++sig->ev_n_signals_added;
    evsig_base_fd = base->sig.ev_signal_pair[0];
    EVSIGBASE_UNLOCK();

    event_debug(("%s: %d: changing signal handler", __func__, (int)evsignal));
        //为该信号设置信号处理函数, 值得注意的是, 是信号处理函数而非事件处理函数
        //差别在于信号处理函数是在管道一端传来数据后,我们通过识别是哪个信号后, 才去调用的处理函数. 而不是通过识别IO复用返回的可用描述符来直接调用处理函数的
    if (_evsig_set_handler(base, (int)evsignal, evsig_handler) == -1) {
        goto err;
    }

    //下面的sig->ev_signal_added是判断整个信号处理是否已经成为IO复用的一个事件
    //看到了吗? 就是在这里添加信号事件在IO复用中的, 也就是说, 只有当用户指定第一个需要监听的信号时, 信号事件才会被放到IO复用中去
    if (!sig->ev_signal_added) {
        if (event_add(&sig->ev_signal, NULL))
            goto err;
        sig->ev_signal_added = 1;
    }

    return (0);

err:
    EVSIGBASE_LOCK();
    --evsig_base_n_signals_added;
    --sig->ev_n_signals_added;
    EVSIGBASE_UNLOCK();
    return (-1);
}




根据以上内容,我们主要查看两个函数, 一个是_evsig_set_handler, 一个是event_add
先看_evsig_set_handler
再次重申一遍, 这个函数的主要目的是为信号设置系统级别的信号处理事件,即一接收到信号就会调用的函数
int
_evsig_set_handler(struct event_base *base,
    int evsignal, void (__cdecl *handler)(int))
{
#ifdef _EVENT_HAVE_SIGACTION
    struct sigaction sa;
#else
    ev_sighandler_t sh;
#endif
    struct evsig_info *sig = &base->sig;
    void *p;

    /*
     * resize saved signal handler array up to the highest signal number.
     * a dynamic array is used to keep footprint on the low side.
     */
        //因为要添加信号处理事件了,所以会记录一下该信号的默认处理方式
        //既然要记录,那么就要看看记录的地方是否有空间可以记录
    if (evsignal >= sig->sh_old_max) {
        int new_max = evsignal + 1;
        event_debug(("%s: evsignal (%d) >= sh_old_max (%d), resizing",
                __func__, evsignal, sig->sh_old_max));
        p = mm_realloc(sig->sh_old, new_max * sizeof(*sig->sh_old));
        if (p == NULL) {
            event_warn("realloc");
            return (-1);
        }

        memset((char *)p + sig->sh_old_max * sizeof(*sig->sh_old),
            0, (new_max - sig->sh_old_max) * sizeof(*sig->sh_old));

        sig->sh_old_max = new_max;
        sig->sh_old = p;
    

    /* allocate space for previous handler out of dynamic array */
        //为这项分配内存, 别忘了sh_old是函数指针数组
    sig->sh_old[evsignal] = mm_malloc(sizeof *sig->sh_old[evsignal]);
    if (sig->sh_old[evsignal] == NULL) {
        event_warn("malloc");
        return (-1);
    }

    /* save previous handler and setup new handler */
#ifdef _EVENT_HAVE_SIGACTION
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler;
    sa.sa_flags |= SA_RESTART;
    sigfillset(&sa.sa_mask);

        //记录下旧的信号抓捕函数, 为该信号设置新的...
    if (sigaction(evsignal, &sa, sig->sh_old[evsignal]) == -1) {
        event_warn("sigaction");
        mm_free(sig->sh_old[evsignal]);
        sig->sh_old[evsignal] = NULL;
        return (-1);
    }
#else
    if ((sh = signal(evsignal, handler)) == SIG_ERR) {
        event_warn("signal");
        mm_free(sig->sh_old[evsignal]);
        sig->sh_old[evsignal] = NULL;
        return (-1);
    }
    *sig->sh_old[evsignal] = sh;
#endif

    return (0);
}
//再看一下设置的handler的函数:
static void __cdecl
evsig_handler(int sig)
{
        //保存错误是一个良好的习惯,因为信号的特殊,其会中断程序的执行,然而信号捕捉函数也可能出错,会设置errnor,而errnor为全局变量,为了不让信号捕捉函数影响程序执行,不丢失得到的错误,所以使用save_errno
    int save_errno = errno;
#ifdef WIN32
    int socket_errno = EVUTIL_SOCKET_ERROR();
#endif
    ev_uint8_t msg;

    if (evsig_base == NULL) {
        event_warnx(
            "%s: received signal %d, but have no base configured",
            __func__, sig);
        return;
    }

#ifndef _EVENT_HAVE_SIGACTION
    signal(sig, evsig_handler);        //现在的signal一般是由sigaction实现的,所以一般不会出现不可靠信号的情况
#endif

    /* Wake up our notification mechanism */
    msg = sig;
    send(evsig_base_fd, (char*)&msg, 1, 0);        //将得到的信号的值发送给管道另一端
    errno = save_errno;
#ifdef WIN32
    EVUTIL_SET_SOCKET_ERROR(socket_errno);
#endif
}


接下来看另一个函数,居然是event_add, 这不是我们添加信号的时候就调用的它嘛!添加到了这时候怎么又碰上它了!怎么成了递归调用了!
event_add ---> event_add_internal ---> evmap_signal_add ---> evsig_add ---> (可能)event_add
于是,我们选择去观察event_add_internal, 发现在我们调用完evmap_signal_add后, 再次调用event_add时我们继续调用的是evmap_io_add,此方法是用来添加普通事件到IO复用的, 不过要清楚的是, 递归调用只会出现一次,因为调用完这一次后ig->ev_signal_added 就被置1了,以后不会再进入那个if了
这正好符合我们对信号的事件源处理的理解, 也符合了我们对此函数的期望:   添加信号处理事件的管道一端到IO复用中去
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值