(十)信號事件集成到多路I/O機制

前言

之前剖析代碼的時候我們知道事件沒激活前,都有自己的數據結構來管理,但是在激活之後都是放在激活鏈表中的。本小節我們將介紹libevent中關於信號事件如何管理,如何將信號事件統一到多路I/O複用事件中一起管理的。
首先我們先介紹一下evsignal_info結構體,再來解析信號事件是如何從註冊到激活,最後被處理的。

evsginal_info

該結構體專門用於關於信號處理。並且每個base對應了一個evsignal_info。其中ev_signal 這個事件是供libevent內部使用的,它在evsignal_init中其事件類型會被設置成永久讀事件,並且其狀態被設置爲內部事件。

struct evsignal_info {
        struct event ev_signal; //註冊讀事件使用的event結構體
        int ev_signal_pair[2];  //socket pair對
        int ev_signal_added;  //標識位,用於記錄該事件是否已經註冊
        volatile sig_atomic_t evsignal_caught;  //標識位,用於記錄是否有信號發生。volatile關鍵字強制cpu每次從內存中讀取數據
        struct event_list evsigevents[NSIG];  //註冊到信號的事件鏈表
        sig_atomic_t evsigcaught[NSIG]; //記錄每個信號觸發的次數(跟ncalls作用相同)
        //sh_old記錄原來的信號處理指針
    #ifdef HAVE_SIGACTION
        struct sigaction **sh_old;
    #else
        ev_sighandler_t **sh_old;
    #endif
        int sh_old_max;
};

如何將信號集成到主循環中

libevent採用的是socketpair的方法。分爲讀socket和寫socket(雖然可以全雙工通信,但是libevent只採用了單向的),讀socket會在event_base上註冊一個讀事件(即ev_signal事件),並給每個需要監聽的信號添加一個共同的信號捕捉函數evsignal_handler,然後設置好處理該信號事件的回調函數。
當信號發生時,evsignal_handler會向讀socket發送1個字節並將該信號對應的evsigncaught加1,代表該信號發生,接着由於由於之前註冊了ev_signal事件,因此該事件被觸發,使得事件分發器知道有事件發生了,於是就可以進行激活進入到激活鏈表中等待處理了,最後調用的是用戶自己設置的信號處理函數。

例子:

SIGINT信號舉例。
1. 首先我們在使用libevent庫時,會調用event_init,它內部會調用event_base_new來創建一個struct event_base *結構體,在event_base_new中,信號相關的會被初始化(evsignal_new),它會創建socketpair對以及設置好內部的ev_signal事件等操作。
2. 我們調用event_set設置好我們的信號事件,struct event中的ev_fd用來記錄信號的編號,即SIGINT的編號。
3. 使用event_add將該信號事件註冊到管理註冊信號的鏈表中並與base綁定。此時event_add內部會檢測到該事件是一個信號事件,因此將它的註冊交給evsignal_add來註冊。evsignal_add會查看SIGINT信號對應的信號事件鏈表是否有事件,如果沒有,給SIGINT註冊一個信號捕捉函數,即evsignal_handler,最後將該信號事件註冊到SIGINT信號對應的事件鏈表上
4. 接着我們調用event_dispatch,其內部最終會調用event_base_loop,即主循環,等待事件發生。當SIGINT信號發送到當前進程時,首先信號捕捉函數開始工作。它將SIGINT信號對應的捕捉次數+1並將有信號發生的標誌位置1(對應evsignal_info中的evsignal_caught),然後向讀socket發送1個字節的數據。
5. 此時,對應的事件分發器(linux下的epoll)得知有事件發生,並且通過判斷evsignal_caught標誌位就可以知道是否有信號觸發,如果有信號觸發,則進入到evsignal_process處理該信號。evsignal_process下一節我們會進行分析,它的主要功能就是遍歷evsigcaught數組,得知哪些信號被觸發了,然後調用event_active將該信號事件加入到激活鏈表中。
6. 最後,eveve_process_active處理激活事件,成功處理掉我們註冊的SIGINT事件。

evsignal_init

在前面的第3小節中,我們提到了event_base_new函數,它是event_init函數中最主要的部分,完成了所有關於event_base的初始化操作。
裏面有一條語句是這樣的base->evbase = base->evsel->init(base),它指向的是某個具體的多路I/O複用機制的初始化函數,在這個初始化函數中,會調用evsignal_init來初始化,它的作用就是初始化信號管理相關的數據。

int
evsignal_init(struct event_base *base)
{
        int i;

        /*
         * 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_warn("%s: socketpair", __func__);
#else
                event_err(1, "%s: socketpair", __func__);
#endif
                return -1;
        }
        //設置FD_CLOEXEC標識,具體作用前面說過,這裏就不說了
        FD_CLOSEONEXEC(base->sig.ev_signal_pair[0]);
        FD_CLOSEONEXEC(base->sig.ev_signal_pair[1]);
        //設置成員的值
        base->sig.sh_old = NULL;
        base->sig.sh_old_max = 0;
        base->sig.evsignal_caught = 0;
        memset(&base->sig.evsigcaught, 0, sizeof(sig_atomic_t)*NSIG);
        /* initialize the queues for all events */
        for (i = 0; i < NSIG; ++i)
                TAILQ_INIT(&base->sig.evsigevents[i]);
        //將設置成非阻塞態
        evutil_make_socket_nonblocking(base->sig.ev_signal_pair[0]);
        //註冊讀事件(注意爲永久事件)
        event_set(&base->sig.ev_signal, base->sig.ev_signal_pair[1],
                EV_READ | EV_PERSIST, evsignal_cb, &base->sig.ev_signal);
        //設置base以及當前事件的狀態
        base->sig.ev_signal.ev_base = base;
        base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL;

        return 0;
}

代碼中涉及到了一個回調函數evsignal_cb,代碼如下:

/* Callback for when the signal handler write a byte to our signaling socket */
static void
evsignal_cb(int fd, short what, void *arg)
{
        static char signals[1];
#ifdef WIN32
        SSIZE_T n;
#else
        ssize_t n;
#endif

        n = recv(fd, signals, sizeof(signals), 0);
        if (n == -1)
                event_err(1, "%s: read", __func__);
}

小結

本小節主要介紹了將信號事件和多路I/O機制聯繫到一起的方法:socket pair,並閱讀了有關信號的結構體以及在event_init中初始化信號函數的代碼。下節我們將繼續探討信號事件註冊、註銷、激活等部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章