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的流程,就留待后续再撰写对应流程来完善说明。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值