libevent信號響應機制的介紹

問題說明

在測試過程中發現了一個進程存在無法被kill 殺死的情況,也就是發送SIGTERM(15)信號後進程無任何響應的情況。因該進程的信號處理機制,是借用了libevent框架實現,懷疑是libevent的信號響應機制沒有生效導致。故而對libevent的生效機制進行適當的分析,發現對信號的捕獲也是在event_base_dispatch中的event_base_loop中實現的,而代碼中存在了一處event_loop_break然後pause的操作。導致libevent的fd監聽機制沒有正常工作。故而捕獲到信號後無法處理,kill無法殺死該使用了libevent信號捕獲機制的進程。下面對libevent捕獲信號的機制做了適當的分析。

libevent信號響應機制使用

libevent中信號處理函數的註冊機制如下所述,這裏捕獲了SIGTERM信號和SIGSEGV信號,註冊了exit_sig_handler函數作爲回調函數,pCtx作爲回調函數使用的參數。pbase爲外部創建的libevent句柄。

	int signo = SIGTERM | SIGSEGV ;
	struct event* ev_signal = evsignal_new(pbase, signo, exit_sig_handler, pCtx);
	evsignal_add(ev_signal, NULL);

該函數的初衷是在異常退出時,將退出前時間點的函數調用棧和內存映射情況打印出來,用作後續的除錯。

libevent信號註冊流程解析

#define evsignal_new(b, x, cb, arg)				\
	event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
    
struct event *
event_new(struct event_base *base, evutil_socket_t fd, short events, void (*cb)(evutil_socket_t, short, void *), void *arg)
{
	struct event *ev;
	ev = mm_malloc(sizeof(struct event));
	if (ev == NULL)
		return (NULL);
	if (event_assign(ev, base, fd, events, cb, arg) < 0) {
		mm_free(ev);
		return (NULL);
	}

	return (ev);
}

evsignal_new創建了事件類型爲EV_SIGNAL的struct event事件句柄,struct event結構體是每個事件的核心結構,在event_assign中被初始化。這裏傳入的fd是上述的signo變量,也就是SIGTERM|SIGSEGV。不同於一般libevent監聽的fd。但這裏的fd最終是賦值給了libevent結構體中的 ev->ev_fd = fd;.

#define evsignal_add(ev, tv)		event_add((ev), (tv))

evsignal_add只是event_add函數的一層封裝,event_add的源碼中,針對EV_SIGNAL類型,最終會調用到evmap_signal_add_。注意這裏區分了io和信號,對io的調用是在evmap_io_add_中註冊的。evmap_signal_add_的定義如下

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);
	}
	GET_SIGNAL_SLOT_AND_CTOR(ctx, map, sig, evmap_signal, evmap_signal_init,
	    base->evsigsel->fdinfo_len);

	if (LIST_EMPTY(&ctx->events)) {
		if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
		    == -1)
			return (-1);
	}

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

	return (1);
}

各位知道struct event_base 結構體變量base相當於是libevent中的last boss,所有的libevent相關的實現都是圍繞着這個數據結構進行的。從這段代碼可以看到與signal信號的捕獲相關的base中的兩個核心的結構體是 evsigsel 和 sigmap,定義如下

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;
    ...
}

可以理解evsigsel這裏註釋是說定義了用於這個event_base對信號做後段處理的函數指針,這裏的struct eventop是和套接字的後段處理回調函數共用的數據結構類型。從op可以理解這個是方法成員(這種用法其實類似於結構體是一個類,而其中的evsigsel結構體指針對應的就是方法,只是這是用c來實現,可以把event_base理解爲就是一個管理對象),也就是函數指針的定義位置。evsignal在如下方法中被定義.

int
evsig_init_(struct event_base *base)
{
...
    /*evsigops爲全局變量,定義如下,包含了關鍵字和回調函數*/
	base->evsigsel = &evsigops;
...
}

static const struct eventop evsigops = {
	"signal",
	NULL,
	evsig_add,
	evsig_del,
	NULL,
	NULL,
	0, 0, 0
};

這裏的evsig_add即上述的evmap_sig_add_的信號註冊函數添加中調用的 if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL).
中的add方法。該方法爲信號添加的最終方法。這個方法在下面的響應機制的解析流程中會再次出現。

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();
	if (evsig_base != base && evsig_base_n_signals_added) {
	...
	}
	evsig_base = base;
	evsig_base_n_signals_added = ++sig->ev_n_signals_added;
	evsig_base_fd = base->sig.ev_signal_pair[1];
	EVSIGBASE_UNLOCK();

	event_debug(("%s: %d: changing signal handler", __func__, (int)evsignal));
	if (evsig_set_handler_(base, (int)evsignal, evsig_handler) == -1) {
		goto err;
	}


	if (!sig->ev_signal_added) {
		if (event_add_nolock_(&sig->ev_signal, NULL, 0))
			goto err;
		sig->ev_signal_added = 1;
	}

	return (0);

evsig_set_handler_函數完成了信號的註冊,完成了添加的核心流程。

libevent信號響應機制解析

針對在libevent框架中註冊信號以及對應的回調處理函數的流程上面已經基本描述完成。讓我們從另一個角度再來看觀察這個流程,就是libevent接收這個響應的流程。
其實也可以猜測到libevent內部應該是使用 sigaction或者signal系統調用實現的該機制。順着這個思路,對實現機制進行分析。對該關鍵字進行查找,可以看到對應的函數,調用signal或者sigaction進行了註冊。在信號觸發時,這裏是系統信號發生時,該進程真正的入口,是調用這裏的handler。(也就是上面部分的結尾)

int
evsig_set_handler_(struct event_base *base,
    int evsignal, void (__cdecl *handler)(int))
{
...
	if (sigaction(evsignal, &sa, sig->sh_old[evsignal]) == -1) {
...
	if ((sh = signal(evsignal, handler)) == SIG_ERR) {

}

evsig_set_handler_該函數追溯調用關係,可以瞭解到是在evsig_add函數中被調用,傳入的handler函數指針爲 evsig_handler函數,定義如下。

static void __cdecl
evsig_handler(int sig)
{
	int save_errno = errno;
	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);
#endif

	/* Wake up our notification mechanism */
	msg = sig;
	{
		int r = write(evsig_base_fd, (char*)&msg, 1);
		(void)r; /* Suppress 'unused return value' and 'unused var' */
	}
	errno = save_errno;
}

可以看到該函數在信號觸發時,會調用evsig_base_fd,而這個evsig_base_fd是什麼呢?。也是在evsig_add中被定義的。

	evsig_base_fd = base->sig.ev_signal_pair[1];

sig.ev_signal_pair[1];這個又是什麼呢?。可以看到 struct event_base 中其實定義了這個sig,即 struct evsig_info。

struct evsig_info {
	/* Event watching ev_signal_pair[1] */
	struct event ev_signal;
	/* Socketpair used to send notifications from the signal handler */
	evutil_socket_t ev_signal_pair[2];
	/* True iff we've added the ev_signal event yet. */
	int ev_signal_added;
	/* Count of the number of signals we're currently watching. */
	int ev_n_signals_added;

	/* Array of previous signal handler objects before Libevent started
	 * messing with them.  Used to restore old signal handlers. */
#ifdef EVENT__HAVE_SIGACTION
	struct sigaction **sh_old;
#else
	ev_sighandler_t **sh_old;
#endif
	/* Size of sh_old. */
	int sh_old_max;
};

這裏的描述是一個用來從信號的回調中發送通知的套接字對,這與我們上面的函數流是一致的。但這個具體是在什麼時候被賦值的,libevent又是如何通過這個來實現處理的呢,這裏我們想到去找他的初始化。在evsig_init_的evutil_make_internal_pipe_中我們可以找到答案。

int
evutil_make_internal_pipe_(evutil_socket_t fd[2])
{
	/*
	  Making the second socket nonblocking is a bit subtle, given that we
	  ignore any EAGAIN returns when writing to it, and you don't usally
	  do that for a nonblocking socket. But if the kernel gives us EAGAIN,
	  then there's no need to add any more data to the buffer, since
	  the main thread is already either about to wake up and drain it,
	  or woken up and in the process of draining it.
	*/

#if defined(EVENT__HAVE_PIPE2)
	if (pipe2(fd, O_NONBLOCK|O_CLOEXEC) == 0)
		return 0;
#endif
#if defined(EVENT__HAVE_PIPE)
	if (pipe(fd) == 0) {
		if (evutil_fast_socket_nonblocking(fd[0]) < 0 ||
		    evutil_fast_socket_nonblocking(fd[1]) < 0 ||
		    evutil_fast_socket_closeonexec(fd[0]) < 0 ||
		    evutil_fast_socket_closeonexec(fd[1]) < 0) {
			close(fd[0]);
			close(fd[1]);
			fd[0] = fd[1] = -1;
			return -1;
		}
		return 0;
	} else {
		event_warn("%s: pipe", __func__);
	}
#endif

可以看到這裏使用了管道技術。pipe2和pipe分別是兩個不同歷史時期的系統調用,但是本質是一致的,只是pipe需要增加部分非阻塞和cloexec的處理。pipe系統調用接受兩個套接字的數組,傳回一個單向的數據通道,fd[0]是隻讀通道,fd[1]是隻寫通道。故而evsig_base_fd被 base->sig.ev_signal_pair[1],即第二個元素賦值的理由。該管道實現了從信號的回調函數,到libevent主循環兩個實體間的通信。

int
evsig_init_(struct event_base *base)
{
...
	if (evutil_make_internal_pipe_(base->sig.ev_signal_pair) == -1) {
...
	event_assign(&base->sig.ev_signal, base, base->sig.ev_signal_pair[0],
		EV_READ | EV_PERSIST, evsig_cb, base);
...
}

在evsig_init中,將管道的只讀端註冊到了整個struct event_base的base中。在隨後的event主循環啓動時,就會在該fd接收到消息時,根據handler傳入的msg也就是觸發的信號,調用對應的處理函數進行處理。
關於event_base_disptach的流程,就留待後續再撰寫對應流程來完善說明。

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