最近接触libevent,拜读“sparkliang”十年前的博文,受益匪浅。但学习中对比libevent-2.1.8 src发现,随着版本演变,文中有些内容不再适用。博客本是很主观的东西,作者书写时的心境已经随风而逝。后来者可进入其当时的心境,但是不可一味盲从甚至奉若神明。
libevent可将IO、定时、信号等事件纳入到主event处理流程event_base_dispatch。现以epoll IO复用为例,梳理下linux下对signal的集成过程。
最开始可追溯到event_init的执行。此函数的功用是new一个struct event_base* base,并初始化某些成员。在这过程中base->evsel = epollops,
const struct eventop epollops = {
"epoll",
epoll_init,
epoll_nochangelist_add,
epoll_nochangelist_del,
epoll_dispatch,
epoll_dealloc,
1, /* need reinit */
EV_FEATURE_ET|EV_FEATURE_O1|EV_FEATURE_EARLY_CLOSE,
0
};
epoll_init也在event_init中被调用,为接下来的架构运行构建基础设施。epoll_init篇幅较长,跟signal有关是它进行了evsig_init_,
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_make_internal_pipe_(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;
}
if (base->sig.sh_old) {
mm_free(base->sig.sh_old);
}
base->sig.sh_old = NULL;
base->sig.sh_old_max = 0;
event_assign(&base->sig.ev_signal, base, base->sig.ev_signal_pair[0],
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;
}
可以留意到有三个地方:
1、evutil_make_internal_pipe_创建了文件描述符对(管道)(socket pair或者pipe2),并赋值给base->sig.ev_signal_pair,用于信号处理函数(下文会提及)和event_base_dispatch事件loop交互。
2、event_assign,构造一个信号struct event,赋给base->sig.ev_signal(回调函数是evsig_cb,注意此时并没有event_add)。
3、初始化base->evsigsel为evsigops。这个用在event_add 函数处理signal时回调。
static const struct eventop evsigops = {
"signal",
NULL,
evsig_add,
evsig_del,
NULL,
NULL,
0, 0, 0
};
evsig_add主要完成两大功能,
1、注册了通用signal的信号处理函数evsig_handler。evsig_handler简单将signal num写入管道写fd
2、event_add_nolock_增加对signal event 管道读fd的监控,获取到达的signal num。
static void __cdecl
evsig_handler(int sig)
{
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);
#endif
/* Wake up our notification mechanism */
msg = sig;
#ifdef _WIN32
send(evsig_base_fd, (char*)&msg, 1, 0);
#else
{
int r = write(evsig_base_fd, (char*)&msg, 1);
(void)r; /* Suppress 'unused return value' and 'unused var' */
}
#endif
errno = save_errno;
#ifdef _WIN32
EVUTIL_SET_SOCKET_ERROR(socket_errno);
#endif
}
如此,基础设施构造完成。下面以文字形式简述signal到来之后发生的种种。
1、signal产生后,evsig_handler被调用,写signal num(char类型消息)到管道写fd。
2、程序主事件loop中对IO处理调用epoll_dispatch,检测到管道读fd可读。经过一系列处理(例如放入active queue),调用event callback - evsig_cb。
3、evsig_cb通过读取管道读fd,获知signal num。再根据signal num检索到用户注册的signal callback,并进行调用。
最后附上evsig_cb的源码
/* Callback for when the signal handler write a byte to our signaling socket */
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];
struct event_base *base;
base = arg;
memset(&ncaught, 0, sizeof(ncaught));
while (1) {
#ifdef _WIN32
n = recv(fd, signals, sizeof(signals), 0);
#else
n = read(fd, signals, sizeof(signals));
#endif
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]++;
}
}
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);
}