前言
之前剖析代碼的時候我們知道事件沒激活前,都有自己的數據結構來管理,但是在激活之後都是放在激活鏈表中的。本小節我們將介紹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
中初始化信號函數的代碼。下節我們將繼續探討信號事件註冊、註銷、激活等部分。