信号
关于统一事件源, 本文是在对其有所了解的情况下进行的,旨在加深对libevent的整体理解
首先,想要了解信号事件的处理,我们需要知道关于信号这部分的接口、内容分别在哪里纵观整个文件夹, 可以看到evsignal-internal.h, signal.c, signal-test.c 这几个文档, 接口在哪里呢?
回忆之前所学内容, 首先肯定是找event_base , 在添加信号事件时会用到信号事件的添加, 在IO复用初始化函数中调用过信号事件的初始化
于是在event_base的定义中我们找到了:
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;
... ...
};
//对于信号处理,添加和删除操作就足够了
static const struct eventop evsigops = {
"signal",
NULL,
evsig_add,
evsig_del,
NULL,
NULL,
0, 0, 0
};
接着, 看struct event_signal_map sigmap, 这个我们肯定有所了解, 这是对应不同信号的处理函数的哈希表, 把所有要处理的信号及其处理函数放在一起等待将来处理
最后, struct evsig_info sig, 这是新内容:
/*我们知道,统一事件源其实就是利用管道, 将得到的信号通过管道发送给管道的一端, 该端被IO复用所监听, 知道是哪个信号后, 调用该信号注册的处理函数即可再来看这个结构体就明了了*/
struct evsig_info {
/* Event watching ev_signal_pair[1] */
/*ev_signal是用来给所有信号在IO复用中注册的, 提供的fd是管道的一端, 一般使用的管道是scoketpair生成的管道,属于全双工管道,所以哪一端传输哪一端接收无关的*/
struct event ev_signal;
/* Socketpair used to send notifications from the signal handler */
/*ev_signal_pair即为管道两端*/
evutil_socket_t ev_signal_pair[2];
/* True iff we've added the ev_signal event yet. */
/*ev_signal_added是用于判断ev_signal是否已经被添加到了IO复用中*/
int ev_signal_added;
/* Count of the number of signals we're currently watching. */
/*ev_n_signals_added表示我们现在在监听几个信号*/
int ev_n_signals_added;
/* Array of previous signal handler objects before Libevent started
* messing with them. Used to restore old signal handlers. */
/*接下来则是sigaction的数组, 用于存储该信号之前的处理函数, 因为如果用户选择放弃监听该信号,那么对该信号的处理函数就要恢复之前的系统默认处理方式,如果不做存储,那在用户放弃监听后就不知道如和处理了*/
#ifdef _EVENT_HAVE_SIGACTION
struct sigaction **sh_old;
#else
ev_sighandler_t **sh_old;
#endif
/* Size of sh_old. */
int sh_old_max;
};
最外层,对所有信号的集中处理
是否还记得在后端IO复用选取的文章中,在解析IO复用的初始化函数时碰到的一行代码:
evsig_init(base);
虽然没有看过其源代码, 但是根据我们的了解,可以模糊的猜测其用意就是在IO复用中注册自己的事件,且提供的描述符是管道的一端,下面就详细的看看:
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_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_sock_warn(-1, "%s: socketpair", __func__);
#else
event_sock_err(1, -1, "%s: socketpair", __func__);
#endif
return -1;
}
//设置在调用exec系列函数时此描述符自动关闭,对于系统编程来说这是一个容易忽略的地方,问题说大不大,说小也不小
//主要是为了防止在子进程中向管道发送数据或其他影响行为
evutil_make_socket_closeonexec(base->sig.ev_signal_pair[0]);
evutil_make_socket_closeonexec(base->sig.ev_signal_pair[1]);
//初始化
base->sig.sh_old = NULL;
base->sig.sh_old_max = 0;
//设置非阻塞
evutil_make_socket_nonblocking(base->sig.ev_signal_pair[0]);
evutil_make_socket_nonblocking(base->sig.ev_signal_pair[1]);
//新建事件,用于将事件放入IO复用中监听
event_assign(&base->sig.ev_signal, base, base->sig.ev_signal_pair[1],
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;
}
//整个初始化函数执行完,我们都没有看到event_add之类的函数将此信号事件放到IO复用中去. 仔细想想, 既然是初始化, 那自然是在用户指定监听哪个信号之前进行的, 此时并没有
//--信号需要监听, 那么又何必提前开始监听信号事件处理函数呢? 那么到底是在什么时候添加这个事件的呢, 下面会看到
//在新建事件时,我们发现注册的事件处理函数名叫 evsig_cb, 拿来看看
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]; //一个int类型的数组,用于捕捉收到的信号
struct event_base *base;
base = arg;
memset(&ncaught, 0, sizeof(ncaught));
while (1) {
n = recv(fd, signals, sizeof(signals), 0); //n表示从管道的一端收到的字节数
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]++; //可以看出,如果接收到某信号,那么对应于数组上的某个位置值就加1
//也可以看出,信号发生几次都详细的记录在案,以后就会调用几次该信号对应的处理事件
}
}
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);
}
信号添加
OK,现在我们知道了关于信号的一些处理,且我们知道添加信号处理和添加普通的文件描述符是不同的,接下来就看看如何将信号与其对应的处理事件添加进去的
于是我们寻找哪里会使用到这个添加函数 event_add ---> event_add_internal ---> evmap_signal_add且添加信号事件,那么肯定是添加在其signal map中的:
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);
}
//下面的这个函数我们之前介绍过,简单的说就是获取该信号对应的TAILQ的头节点,用ctx指向
GET_SIGNAL_SLOT_AND_CTOR(ctx, map, sig, evmap_signal, evmap_signal_init,
base->evsigsel->fdinfo_len);
//既然该信号的事件队列为空,那么就说明,可能其他所有信号的事件都是空的,(注意是可能)这样一来,我们又知道在init函数中并没有将信号事件添加到IO复用中去,于是在这里我们就可能要添加
//还有一点在于信号本身的处理函数,作为事件源处理方式,我们知道IO复用通过管道一端传来的数据判断是哪个信号. 可管道的另一端是怎样神秘的存在呢?它就是一般在系统编程中的信号处理函数,要注意的是这里的信号处理函数和我们讲的libevent中的信号处理函数并不同。 系统编程中的信号处理函数调用write管道将信号的值(int类型)传给IO复用, IO复用读取管道得到该信号的值,于是再调用libevent中的信号处理函数进行处理
//之所以这么认为,还因为下面已经有插入操作了,所以这里才如此揣测evsel->add
if (TAILQ_EMPTY(&ctx->events)) {
if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
== -1)
return (-1);
}
TAILQ_INSERT_TAIL(&ctx->events, ev, ev_signal_next);
return (1);
}
再次回顾信号的抽象操作结构体定义:
static const struct eventop evsigops = {
"signal",
NULL,
evsig_add,
evsig_del,
NULL,
NULL,
0, 0, 0
};
其中add操作对应的函数即为evsig_add
//是如何调用的:evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
//根据以上的分析,evsig_add函数主要两个目的是: 1、添加信号处理事件的管道一端到IO复用中去 2、为信号设置系统级别的处理函数
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();
//下面的判断条件是 1、信号接收的base和当前作为参数传入的base不一致 2、监听的信号数量大于0
//下面两个是全局变量, 被初始化为NULL 和 0
//在这之前,我们并未遇到这两个变量,这里是第一次,于是这个判断在第一次添加一个信号的时候不会进入
if (evsig_base != base && evsig_base_n_signals_added) {
//下面这段话的意思是, 一个进程只能有一个base可以接收信号, 这个base应该是最近刚添加过信号的或是最近刚调用event_base_loop的那个
event_warnx("Added a signal to event base %p with signals "
"already added to event_base %p. Only one can have "
"signals at a time with the %s backend. The base with "
"the most recently added signal or the most recent "
"event_base_loop() call gets preference; do "
"not rely on this behavior in future Libevent versions.",
base, evsig_base, base->evsel->name);
}
//设置全局变量的值
evsig_base = base;
evsig_base_n_signals_added = ++sig->ev_n_signals_added;
evsig_base_fd = base->sig.ev_signal_pair[0];
EVSIGBASE_UNLOCK();
event_debug(("%s: %d: changing signal handler", __func__, (int)evsignal));
//为该信号设置信号处理函数, 值得注意的是, 是信号处理函数而非事件处理函数
//差别在于信号处理函数是在管道一端传来数据后,我们通过识别是哪个信号后, 才去调用的处理函数. 而不是通过识别IO复用返回的可用描述符来直接调用处理函数的
if (_evsig_set_handler(base, (int)evsignal, evsig_handler) == -1) {
goto err;
}
//下面的sig->ev_signal_added是判断整个信号处理是否已经成为IO复用的一个事件
//看到了吗? 就是在这里添加信号事件在IO复用中的, 也就是说, 只有当用户指定第一个需要监听的信号时, 信号事件才会被放到IO复用中去
if (!sig->ev_signal_added) {
if (event_add(&sig->ev_signal, NULL))
goto err;
sig->ev_signal_added = 1;
}
return (0);
err:
EVSIGBASE_LOCK();
--evsig_base_n_signals_added;
--sig->ev_n_signals_added;
EVSIGBASE_UNLOCK();
return (-1);
}
根据以上内容,我们主要查看两个函数, 一个是_evsig_set_handler, 一个是event_add
先看_evsig_set_handler
再次重申一遍, 这个函数的主要目的是为信号设置系统级别的信号处理事件,即一接收到信号就会调用的函数
int
_evsig_set_handler(struct event_base *base,
int evsignal, void (__cdecl *handler)(int))
{
#ifdef _EVENT_HAVE_SIGACTION
struct sigaction sa;
#else
ev_sighandler_t sh;
#endif
struct evsig_info *sig = &base->sig;
void *p;
/*
* resize saved signal handler array up to the highest signal number.
* a dynamic array is used to keep footprint on the low side.
*/
//因为要添加信号处理事件了,所以会记录一下该信号的默认处理方式
//既然要记录,那么就要看看记录的地方是否有空间可以记录
if (evsignal >= sig->sh_old_max) {
int new_max = evsignal + 1;
event_debug(("%s: evsignal (%d) >= sh_old_max (%d), resizing",
__func__, evsignal, sig->sh_old_max));
p = mm_realloc(sig->sh_old, new_max * sizeof(*sig->sh_old));
if (p == NULL) {
event_warn("realloc");
return (-1);
}
memset((char *)p + sig->sh_old_max * sizeof(*sig->sh_old),
0, (new_max - sig->sh_old_max) * sizeof(*sig->sh_old));
sig->sh_old_max = new_max;
sig->sh_old = p;
/* allocate space for previous handler out of dynamic array */
//为这项分配内存, 别忘了sh_old是函数指针数组
sig->sh_old[evsignal] = mm_malloc(sizeof *sig->sh_old[evsignal]);
if (sig->sh_old[evsignal] == NULL) {
event_warn("malloc");
return (-1);
}
/* save previous handler and setup new handler */
#ifdef _EVENT_HAVE_SIGACTION
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
//记录下旧的信号抓捕函数, 为该信号设置新的...
if (sigaction(evsignal, &sa, sig->sh_old[evsignal]) == -1) {
event_warn("sigaction");
mm_free(sig->sh_old[evsignal]);
sig->sh_old[evsignal] = NULL;
return (-1);
}
#else
if ((sh = signal(evsignal, handler)) == SIG_ERR) {
event_warn("signal");
mm_free(sig->sh_old[evsignal]);
sig->sh_old[evsignal] = NULL;
return (-1);
}
*sig->sh_old[evsignal] = sh;
#endif
return (0);
}
//再看一下设置的handler的函数:
static void __cdecl
evsig_handler(int sig)
{
//保存错误是一个良好的习惯,因为信号的特殊,其会中断程序的执行,然而信号捕捉函数也可能出错,会设置errnor,而errnor为全局变量,为了不让信号捕捉函数影响程序执行,不丢失得到的错误,所以使用save_errno
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); //现在的signal一般是由sigaction实现的,所以一般不会出现不可靠信号的情况
#endif
/* Wake up our notification mechanism */
msg = sig;
send(evsig_base_fd, (char*)&msg, 1, 0); //将得到的信号的值发送给管道另一端
errno = save_errno;
#ifdef WIN32
EVUTIL_SET_SOCKET_ERROR(socket_errno);
#endif
}
接下来看另一个函数,居然是event_add, 这不是我们添加信号的时候就调用的它嘛!添加到了这时候怎么又碰上它了!怎么成了递归调用了!
event_add ---> event_add_internal ---> evmap_signal_add ---> evsig_add ---> (可能)event_add
于是,我们选择去观察event_add_internal, 发现在我们调用完evmap_signal_add后, 再次调用event_add时我们继续调用的是evmap_io_add,此方法是用来添加普通事件到IO复用的, 不过要清楚的是, 递归调用只会出现一次,因为调用完这一次后ig->ev_signal_added 就被置1了,以后不会再进入那个if了
这正好符合我们对信号的事件源处理的理解, 也符合了我们对此函数的期望: 添加信号处理事件的管道一端到IO复用中去