前言
之前剖析代码的时候我们知道事件没激活前,都有自己的数据结构来管理,但是在激活之后都是放在激活链表中的。本小节我们将介绍libevent中关于信号事件如何管理,如何将信号事件统一到多路I/O复用事件中一起管理的。
首先我们先介绍一下evsignal_info
结构体,再来解析信号事件是如何从注册到激活,最后被处理的。
evsginal_info
该结构体专门用于关于信号处理。并且每个base对应了一个evsignal_info
。其中ev_signal
这个事件是供libevent内部使用的,它在evsignal_init
中其事件类型会被设置成永久读事件,并且其状态被设置为内部事件。
struct evsignal_info {
struct event ev_signal; //注册读事件使用的event结构体
int ev_signal_pair[2]; //socket pair对
int ev_signal_added; //标识位,用于记录该事件是否已经注册
volatile sig_atomic_t evsignal_caught; //标识位,用于记录是否有信号发生。volatile关键字强制cpu每次从内存中读取数据
struct event_list evsigevents[NSIG]; //注册到信号的事件链表
sig_atomic_t evsigcaught[NSIG]; //记录每个信号触发的次数(跟ncalls作用相同)
//sh_old记录原来的信号处理指针
#ifdef HAVE_SIGACTION
struct sigaction **sh_old;
#else
ev_sighandler_t **sh_old;
#endif
int sh_old_max;
};
如何将信号集成到主循环中
libevent采用的是socketpair
的方法。分为读socket和写socket(虽然可以全双工通信,但是libevent只采用了单向的),读socket会在event_base上注册一个读事件(即ev_signal
事件),并给每个需要监听的信号添加一个共同的信号捕捉函数evsignal_handler
,然后设置好处理该信号事件的回调函数。
当信号发生时,evsignal_handler
会向读socket发送1个字节并将该信号对应的evsigncaught
加1,代表该信号发生,接着由于由于之前注册了ev_signal
事件,因此该事件被触发,使得事件分发器知道有事件发生了,于是就可以进行激活进入到激活链表中等待处理了,最后调用的是用户自己设置的信号处理函数。
例子:
拿SIGINT
信号举例。
1. 首先我们在使用libevent库时,会调用event_init
,它内部会调用event_base_new
来创建一个struct event_base *
结构体,在event_base_new
中,信号相关的会被初始化(evsignal_new
),它会创建socketpair对以及设置好内部的ev_signal
事件等操作。
2. 我们调用event_set
设置好我们的信号事件,struct event
中的ev_fd
用来记录信号的编号,即SIGINT
的编号。
3. 使用event_add
将该信号事件注册到管理注册信号的链表中并与base
绑定。此时event_add
内部会检测到该事件是一个信号事件,因此将它的注册交给evsignal_add
来注册。evsignal_add
会查看SIGINT
信号对应的信号事件链表是否有事件,如果没有,给SIGINT
注册一个信号捕捉函数,即evsignal_handler
,最后将该信号事件注册到SIGINT
信号对应的事件链表上
4. 接着我们调用event_dispatch
,其内部最终会调用event_base_loop
,即主循环,等待事件发生。当SIGINT
信号发送到当前进程时,首先信号捕捉函数开始工作。它将SIGINT
信号对应的捕捉次数+1并将有信号发生的标志位置1(对应evsignal_info
中的evsignal_caught
),然后向读socket发送1个字节的数据。
5. 此时,对应的事件分发器(linux下的epoll)得知有事件发生,并且通过判断evsignal_caught
标志位就可以知道是否有信号触发,如果有信号触发,则进入到evsignal_process
处理该信号。evsignal_process
下一节我们会进行分析,它的主要功能就是遍历evsigcaught
数组,得知哪些信号被触发了,然后调用event_active
将该信号事件加入到激活链表中。
6. 最后,eveve_process_active
处理激活事件,成功处理掉我们注册的SIGINT
事件。
evsignal_init
在前面的第3小节中,我们提到了event_base_new
函数,它是event_init
函数中最主要的部分,完成了所有关于event_base
的初始化操作。
里面有一条语句是这样的base->evbase = base->evsel->init(base)
,它指向的是某个具体的多路I/O复用机制的初始化函数,在这个初始化函数中,会调用evsignal_init
来初始化,它的作用就是初始化信号管理相关的数据。
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) {
#ifdef WIN32
/* Make this nonfatal on win32, where sometimes people
have localhost firewalled. */
event_warn("%s: socketpair", __func__);
#else
event_err(1, "%s: socketpair", __func__);
#endif
return -1;
}
//设置FD_CLOEXEC标识,具体作用前面说过,这里就不说了
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]);
//注册读事件(注意为永久事件)
event_set(&base->sig.ev_signal, base->sig.ev_signal_pair[1],
EV_READ | EV_PERSIST, evsignal_cb, &base->sig.ev_signal);
//设置base以及当前事件的状态
base->sig.ev_signal.ev_base = base;
base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL;
return 0;
}
代码中涉及到了一个回调函数evsignal_cb
,代码如下:
/* Callback for when the signal handler write a byte to our signaling socket */
static void
evsignal_cb(int fd, short what, void *arg)
{
static char signals[1];
#ifdef WIN32
SSIZE_T n;
#else
ssize_t n;
#endif
n = recv(fd, signals, sizeof(signals), 0);
if (n == -1)
event_err(1, "%s: read", __func__);
}
小结
本小节主要介绍了将信号事件和多路I/O机制联系到一起的方法:socket pair
,并阅读了有关信号的结构体以及在event_init
中初始化信号函数的代码。下节我们将继续探讨信号事件注册、注销、激活等部分。