Tips: 关联signal到event base的api主要在
evsignal.h
中
Note: 以下封装的系统I\O mutiplexing具体实现均以epoll为例子
集成signal处理的核心信息在evsignal_info结构体中
struct evsignal_info {
struct event ev_signal; // "读socket"事件,用于关联signal到事件框架中
int ev_signal_pair[2]; // socket对,用于关联signal到事件框架中
int ev_signal_added; // 标识是否已注册"读socket到base"中.
volatile sig_atomic_t evsignal_caught;// 标识base中信号发生, 0无1有
struct event_list evsigevents[NSIG]; // evsigevents[sig]表示注册到sig信号的事件集合
sig_atomic_t evsigcaught[NSIG]; // evsigcaught[sig]表示记录信号sig的触发次数
#ifdef HAVE_SIGACTION
struct sigaction **sh_old; // sh_old数组,记录原有的信号处理的函数指针
#else // 当signal事件从base注销后,需要恢复原有的signal handler
ev_sighandler_t **sh_old;
#endif
int sh_old_max; // sh_old的容量大小
};
在eventop::init
中,通过evsignal_init
调用evutil_socketpair
创建socket pair.
evsignal_init 职责: 初始化struct evsignal_info:
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) {
return -1;
}
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]);
evutil_make_socket_nonblocking(base->sig.ev_signal_pair[1]);
// 初始化socket pair读事件,
// 实际上直到base被注册signal事件才将读socket注册到base中
// 详见evsig_add()
event_set(&base->sig.ev_signal, base->sig.ev_signal_pair[1],
EV_READ | EV_PERSIST, evsignal_cb, &base->sig.ev_signal);
base->sig.ev_signal.ev_base = base;
base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL;
return 0;
}
Note: evsignal_cb 几乎什么也没做
懒惰加载——注册读端
在eventop::add
(以epoll_add
为例)只有观察到用户注册signal event到base时,才将读端event关联到base中.
static int
epoll_add(void *arg, struct event *ev)
{
struct epollop *epollop = arg;
struct epoll_event epev = {0, {0}};
struct evepoll *evep;
int fd, op, events;
if (ev->ev_events & EV_SIGNAL)
return (evsignal_add(ev));
/*......*/
return (0);
}
int
evsignal_add(struct event *ev)
{
int evsignal;
struct event_base *base = ev->ev_base;
struct evsignal_info *sig = &ev->ev_base->sig;
// 事件不能即使I/O事件又是signal事件(check valid)
if (ev->ev_events & (EV_READ|EV_WRITE))
event_errx(1, "%s: EV_SIGNAL incompatible use", __func__);
// 检测signal事件的ev_fd合法性(必须是系统支持的信号类型范围)
// evsignal相当于信号的类型的数值signo
evsignal = EVENT_SIGNAL(ev);
assert(evsignal >= 0 && evsignal < NSIG);
// 首次添加该信号事件
if (TAILQ_EMPTY(&sig->evsigevents[evsignal])) {
if (_evsignal_set_handler(
base, evsignal, evsignal_handler) == -1)
return (-1);
/* catch signals if they happen quickly */
evsignal_base = base;
// 首次注册"读socket"事件到base中(懒惰加载)
if (!sig->ev_signal_added) {
if (event_add(&sig->ev_signal, NULL))
return (-1);
sig->ev_signal_added = 1;
}
}
/* multiple events may listen to the same signal */
TAILQ_INSERT_TAIL(&sig->evsigevents[evsignal], ev, ev_signal_next);
return (0);
}
通过socket pair关联signal event处理。
一个做读端一个做写端。eventop::dispatch
接受到信号中断后,在signal handler(evsignal_handler
)中往写端写入数据从而唤醒读端event.
当signal被接受时,不是立刻调用相应的signal handler,而是仅仅通过evsignal_handler
告知base,这个signal接收了几次,再通过唤醒读端event告知base并通过evsignal_process
将触发的信号事件加入到激活链表中, 最后在事件主循环中对signal事件和I\O事件集中统一处理。
思路脉络:
evsignal_handler的职责:
/**
* 用于注册到signal(2)的handler
*
* 职责:
* 1. 当系统处理信号中断时,维护base中的signal事件触发相关信息
* 2. 通过"写socket"唤醒注册到base中的"读socket"处理信号事件
*/
static void
evsignal_handler(int sig)
{
int save_errno = errno;
if (evsignal_base == NULL) {
event_warn(
"%s: received signal %d, but have no base configured",
__func__, sig);
return;
}
// sig触发次数自增
evsignal_base->sig.evsigcaught[sig]++;
// 标记base中有signal事件被触发
evsignal_base->sig.evsignal_caught = 1;
// 若系统不支持sigaction函数,libevent将做什么处理?
#ifndef HAVE_SIGACTION
// 重新注册信号
signal(sig, evsignal_handler);
#endif
/* Wake up our notification mechanism */
// 向"写socket"写入数据唤醒"读socket"
send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
errno = save_errno;
}
在libevent接受信号后(即控制回到程序段eventop::dispatch
)
1.系统I/O检测到读端事件的到达,从阻塞中恢复
2.随后evsignal_process
处理信息事件。
evsignal_process
的职责:
将所有触发的信号放入激活链表中
/**
* 遍历所有信号类型的触发次数
* 针对被触发的信号,遍历注册该信号的事件链表
* 并将相关事件添加到激活链表中
*/
void
evsignal_process(struct event_base *base)
{
struct evsignal_info *sig = &base->sig;
struct event *ev, *next_ev;
sig_atomic_t ncalls;
int i;
// 重置信号触发标识
base->sig.evsignal_caught = 0;
for (i = 1; i < NSIG; ++i) {
ncalls = sig->evsigcaught[i];
if (ncalls == 0)
continue;
// 为什么不直接置0而是减ncals呢?
// 猜测由于signal到达时机不明确,
// 可能导致竟态问题
sig->evsigcaught[i] -= ncalls;
// 遍历注册了该信号的事件链表
for (ev = TAILQ_FIRST(&sig->evsigevents[i]);
ev != NULL; ev = next_ev) {
next_ev = TAILQ_NEXT(ev, ev_signal_next);
// 非持久事件直接删除
if (!(ev->ev_events & EV_PERSIST))
event_del(ev);
// 添加信号事件到激活链表
event_active(ev, EV_SIGNAL, ncalls);
}
}
}
Q: eventop::dispatch
已被信号中断,通过写端唤醒读端还有必要的吗?
A: 有必要。eventop::dispatch
会自动重启(设置了SA_RESTART)时,就需要“读socket"即时唤醒。这其实与可移植性有关,系统的信号处理语义不同系统中的实现不明确,libevent对此做了调整,能重启被中断的系统调用尽量重启。