信号event的工作原理:
前面讲解了Libevent怎么对一个IO事件进行监听,现在来讲一下Libevent怎么监听信号。Libevent对于信号的处理是采用统一事件源的方式。简单地说,就是把信号也转换成IO事件,集成到Libevent中。
统一事件源的工作原理是这样的:假如用户要监听SIGINT这个信号,那么在实现的内部就对SIGINT这个信号设置捕抓函数。此外,在实现的内部还要建立一条管道(pipe),并把这个管道加入到多路IO复用函数中。当SIGINT这个信号发生后,捕抓函数将会被调用。而这个捕抓函数的工作就是往管道写入一个字符(这个字符往往等于所捕抓到信号的信号值)。此时,这个管道就变成是可读的了,多路IO复用函数能检测到这个管道变成可读的了。换句话说就是多路IO复用函数检测到这个SIGINT信号发生了,这也就完成了对信号的监听工作。这个过程如下图所示:
读者现在应该都明白了统一事件源的工作原理。现在来看一下Libevent具体是怎么实现的。从上面所说的,可以知道所需的工作有:
- 创建一个管道(Libevent实际上使用的是socketpair)
- 为这个socketpair的一个读端创建一个event,并将之加入到多路IO复用函数的监听之中
- 设置信号捕抓函数
- 有信号发生,就往socketpair写入一个字节
统一事件源能够工作的一个原因是:多路IO复用函数都是可中断的。即处理完信号后,会从多路IO复用函数中退出,并将errno赋值为EINTR。有些OS的某些系统调用,比如Linux的read,即使被信号终端了,还是会自启动的。即不会从read函数中退出来。
用于信号event的结构体和变量:
在说之前,看一下event_base在信号监听这方面提供了什么的成员。
-
- struct event_base {
-
- const struct eventop *evsigsel;
- struct evsig_info sig;
-
- ...
- struct event_signal_map sigmap;
- ...
- };
-
-
-
- struct evsig_info {
-
- struct event ev_signal;
-
- evutil_socket_t ev_signal_pair[2];
-
- int ev_signal_added;
-
- int ev_n_signals_added;
-
-
-
-
-
-
- #ifdef _EVENT_HAVE_SIGACTION
- struct sigaction **sh_old;
- #else//保存的是捕抓函数的函数指针,又因为是数组。所以是二级指针
- ev_sighandler_t **sh_old;
- #endif
-
- int sh_old_max;
- };
在上面代码中,已经可以看到用于socketpair的ev_signal_pair变量,还有struct event结构体变量ev_signal。那么Libevent是在什么时候创建socketpair以及怎么将socketpair和ev_signal相关联的呢?
初始化:
在前面的博文《 跨平台Reactor接口的实现
》中,有说到Libevent是怎么选择一个多路IO复用函数的。在选定一个多路IO复用函数后,就会调用下面一行代码。
- base->evbase = base->evsel->init(base);
这是初始化代码函数。下面给出poll的init函数。
-
- static void *
- poll_init(struct event_base *base)
- {
- struct pollop *pollop;
-
- if (!(pollop = mm_calloc(1, sizeof(struct pollop))))
- return (NULL);
-
- evsig_init(base);
-
- return (pollop);
- }
可以看到,其调用了evsig_init函数。而正是这个evsig_init函数完成了创建socketpair并将socketpair的一个读端与ev_signal相关联。
-
- int
- evsig_init(struct event_base *base)
- {
-
- if (evutil_socketpair(
- AF_UNIX, SOCK_STREAM, 0, base->sig.ev_signal_pair) == -1) {
- #ifdef WIN32
-
-
- event_sock_warn(-1, "%s: socketpair", __func__);
- #else
- event_sock_err(1, -1, "%s: socketpair", __func__);
- #endif
- return -1;
- }
-
-
- 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]);
-
-
-
- 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;
- }
socketpair的两个端都调用evutil_make_socket_closeonexec,因为不能让子进程可以访问的这个socketpair。因为子进程的访问可能会出现扰乱。比如,子进程往socketpair发送信息,使得父进程的多路IO复用函数误以为信号发生了;父进程确实发生了信号,也往socketpair发送了一个字节,但却被子进程接收了这个字节。父进程没有监听到可读。
在Windows中,并没有直接的可以使用的socketpair API。此时,Libevent就自己实现了一个socketpair。具体可以参考《通用类型和函数》。
在函数的最后可以看到event_base的一个成员evsignal被赋值。evsignal是一个IO复用结构体,而evsigops是专门用于信号处理的 IO复用结构体
变量。定义如下:
-
- static const struct eventop evsigops = {
- "signal",
- NULL,
- evsig_add,
- evsig_del,
- NULL,
- NULL,
- 0, 0, 0
- };
该结构体只有evsig_add和evsig_del这两个函数指针。实际在工作时有这两个函数就足够了。
将信号event加入到event_base:
前面的代码已经完成了“创建socketpair并将socketpair的一个读端于ev_signal相关联”。接下来看其他的工作。假如要对一个绑定了某个信号的event调用event_add函数,那么在event_add的内部会调用event_add_internal函数。而event_add_internal函数又会调用evmap_signal_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;
-
- ...
-
- if (TAILQ_EMPTY(&ctx->events)) {
-
- if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
- == -1)
- return (-1);
- }
-
- return (1);
- }
上面函数的内部调用了IO复用结构体的add函数指针,即调用了evsig_add。现在我们深入evsig_add函数。
- /signal.c文件
- 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);
-
-
-
-
- EVSIGBASE_LOCK();
-
- if (evsig_base != base && evsig_base_n_signals_added) {
- 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();
-
-
- if (_evsig_set_handler(base, (int)evsignal, evsig_handler) == -1) {
- goto err;
- }
-
-
- 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);
- }
从后面的那个if语句可以得知,当sig->ev_signal_added变量为0时(即用户第一次监听一个信号),就会将ev_signal这个event加入到event_base中。从前面的“统一事件源”可以得知,这个ev_signal的作用就是通知event_base,有信号发生了。只需一个event即可完成工作,即使用户要监听多个不同的信号,因为这个event已经和socketpair的读端相关联了。如果要监听多个信号,那么就在信号处理函数中往这个socketpair写入不同的值即可。event_base能监听到可读,并可以从读到的内容可以判断是哪个信号发生了。
从代码中也可得知,Libevent并不会为每一个信号监听创建一个event。它只会创建一个全局的专门用于监听信号的event。这个也是“统一事件源”的工作原理。
设置信号捕抓函数:
evsig_add函数还调用了_evsig_set_handler函数完成设置Libevent内部的信号捕抓函数。
-
- typedef void (*ev_sighandler_t)(int);
-
-
- 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;
-
-
-
- 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;
- }
-
-
-
- sig->sh_old[evsignal] = mm_malloc(sizeof *sig->sh_old[evsignal]);
- if (sig->sh_old[evsignal] == NULL) {
- event_warn("malloc");
- return (-1);
- }
-
-
- #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);
- }
如果看过《UNIX环境高级编程》信号那章的话,上面这段代码很容易看懂。这里就不讲了。
这里我们做一个猜测:当我们对某个信号进行event_new和event_add后,就不应该再次设置该信号的信号捕抓函数。否则event_base将无法监听到信号的发生。下面代码验证这猜测。
- #include<unistd.h>
- #include<stdio.h>
- #include<signal.h>
- #include<event.h>
-
-
- void sig_cb(int fd, short events, void *arg)
- {
- printf("in the sig_cb\n");
- }
-
- void signal_handle(int sig)
- {
- printf("catch the sig %d\n", sig);
- }
-
- int main()
- {
-
- struct event_base *base = event_base_new();
-
- struct event *ev = evsignal_new(base, SIGUSR1, sig_cb, NULL);
- event_add(ev, NULL);
-
- signal(SIGUSR1, signal_handle);
-
- printf("pid = %d\n", getpid());
-
- printf("begin\n");
- event_base_dispatch(base);
- printf("end\n");
-
- return 0;
- }
运行上面代码, 通过在外部给这个进程发生信号的方式。可以看到,event_base确实无法监听到信号了。所有信号都被signal_handle捕抓了。
捕抓信号:
前面的代码中有两个函数并没有讲,分别是信号捕抓函数evsig_handler和调用event_assign时的信号回调函数evsig_cb。
-
- static void __cdecl
- evsig_handler(int sig)
- {
- ...
- 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
-
-
- msg = sig;
- send(evsig_base_fd, (char*)&msg, 1, 0);
-
- ...
- }
从evsig_handler函数的实现可以看到,实现得相当简单。只是将信号对应的值写入到socketpair中。evsig_base_fd是socketpair的写端,这是一个全局变量,在evsig_add函数中被赋值的。
从“统一事件源”的工作原理来看,现在已经完成了对信号的捕抓,已经将该信号的当作IO事件写入到socketpair中了。现在event_base应该已经监听到socketpair可读了,并且会为调用回调函数evsig_cb了。下面看看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];
- struct event_base *base;
-
- base = arg;
-
- memset(&ncaught, 0, sizeof(ncaught));
-
- while (1) {
-
-
-
- n = recv(fd, signals, sizeof(signals), 0);
- 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) {
-
- 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);
- }
该回调函数的作用是读取socketpair的所有数据,并将数据当作信号,再根据信号值调用evmap_signal_active。
有一点要注意,evsig_cb这个回调函数并不是用户为监听一个信号调用event_new时设置的用户回调函数,而是Libevent内部为了处理信号而设置的内部回调函数。累!!
激活信号event:
虽然如此,但是现在的情况是:当有信号发生时,就会调用evmap_signal_active函数。
-
- #define ev_signal_next _ev.ev_signal.ev_signal_next
-
- #define ev_ncalls _ev.ev_signal.ev_ncalls
- #define ev_pncalls _ev.ev_signal.ev_pncalls
-
-
- void
- evmap_signal_active(struct event_base *base, evutil_socket_t sig, int ncalls)
- {
- struct event_signal_map *map = &base->sigmap;
- struct evmap_signal *ctx;
- struct event *ev;
-
-
- GET_SIGNAL_SLOT(ctx, map, sig, evmap_signal);
-
-
- TAILQ_FOREACH(ev, &ctx->events, ev_signal_next)
- event_active_nolock(ev, EV_SIGNAL, ncalls);
- }
-
-
-
- void
- event_active_nolock(struct event *ev, int res, short ncalls)
- {
- struct event_base *base;
-
-
- base = ev->ev_base;
- ev->ev_res = res;
-
-
- if (ev->ev_pri < base->event_running_priority)
- base->event_continue = 1;
-
- if (ev->ev_events & EV_SIGNAL) {
- #ifndef _EVENT_DISABLE_THREAD_SUPPORT
- if (base->current_event == ev && !EVBASE_IN_THREAD(base)) {
- ++base->current_event_waiters;
-
- EVTHREAD_COND_WAIT(base->current_event_cond, base->th_base_lock);
- }
- #endif
- ev->ev_ncalls = ncalls;
- ev->ev_pncalls = NULL;
- }
-
-
- event_queue_insert(base, ev, EVLIST_ACTIVE);
-
- }
通过evmap_signal_active、event_active_nolock和event_queue_insert这三个函数的调用后,就可以把一个event插入到激活队列了。
由于这些函数的执行本身就是在Libevent处理event的回调函数之中的(Libevent正在处理内部的信号处理event)。所以并不需要从event_base_loop里的while循环里面再次执行一次evsel->dispatch(),才能执行到这次信号event。即无需等到下一次处理激活队列,就可以执行该信号event了。分析如下:
首先要明确,现在执行上面三个函数相当于在执行event的回调函数。所以其是运行在event_process_active函数之中的。为什么是在这里,可以参考《Libevent工作流程探究》一文。
-
- static int
- event_process_active(struct event_base *base)
- {
- struct event_list *activeq = NULL;
- int i, c = 0;
-
-
- for (i = 0; i < base->nactivequeues; ++i) {
-
- if (TAILQ_FIRST(&base->activequeues[i]) != NULL) {
- base->event_running_priority = i;
- activeq = &base->activequeues[i];
- c = event_process_active_single_queue(base, activeq);
- }
- }
-
- return c;
- }
-
- static int
- event_process_active_single_queue(struct event_base *base,
- struct event_list *activeq)
- {
- struct event *ev;
-
-
- for (ev = TAILQ_FIRST(activeq); ev; ev = TAILQ_FIRST(activeq)) {
-
- ...
- }
-
- }
从上面的代码可以看到,Libevent在处理内部的那个信号处理event的回调函数时,其实是在event_process_active_single_queue的一个循环里面。因为Libevent内部的信号处理event的优先级最高优先级,并且在前面的将用户信号event插入到队列(即event_queue_insert),在插入到队列的尾部。所以无论用户的这个信号event的优先级是多少,都是在Libevent的内部信号处理event的后面。所以在遍历上面两个函数的里外两个循环时,肯定会执行到用户的信号event。
执行已激活信号event:
现在看看Libevent是怎么处理已激活的信号event的。
-
- static inline void
- event_signal_closure(struct event_base *base, struct event *ev)
- {
- short ncalls;
- int should_break;
-
-
- ncalls = ev->ev_ncalls;
- if (ncalls != 0)
- ev->ev_pncalls = &ncalls;
-
-
-
- EVBASE_RELEASE_LOCK(base, th_base_lock);
-
- while (ncalls) {
- ncalls--;
- ev->ev_ncalls = ncalls;
- if (ncalls == 0)
- ev->ev_pncalls = NULL;
- (*ev->ev_callback)(ev->ev_fd, ev->ev_res, ev->ev_arg);
-
- EVBASE_ACQUIRE_LOCK(base, th_base_lock);
-
- should_break = base->event_break;
- EVBASE_RELEASE_LOCK(base, th_base_lock);
-
- if (should_break) {
- if (ncalls != 0)
- ev->ev_pncalls = NULL;
- return;
- }
- }
- }
可以看到,如果对应的信号发生了多次,那么该信号event的回调函数将被执行多次。
信号event的工作原理:
前面讲解了Libevent怎么对一个IO事件进行监听,现在来讲一下Libevent怎么监听信号。Libevent对于信号的处理是采用统一事件源的方式。简单地说,就是把信号也转换成IO事件,集成到Libevent中。
统一事件源的工作原理是这样的:假如用户要监听SIGINT这个信号,那么在实现的内部就对SIGINT这个信号设置捕抓函数。此外,在实现的内部还要建立一条管道(pipe),并把这个管道加入到多路IO复用函数中。当SIGINT这个信号发生后,捕抓函数将会被调用。而这个捕抓函数的工作就是往管道写入一个字符(这个字符往往等于所捕抓到信号的信号值)。此时,这个管道就变成是可读的了,多路IO复用函数能检测到这个管道变成可读的了。换句话说就是多路IO复用函数检测到这个SIGINT信号发生了,这也就完成了对信号的监听工作。这个过程如下图所示:
读者现在应该都明白了统一事件源的工作原理。现在来看一下Libevent具体是怎么实现的。从上面所说的,可以知道所需的工作有:
- 创建一个管道(Libevent实际上使用的是socketpair)
- 为这个socketpair的一个读端创建一个event,并将之加入到多路IO复用函数的监听之中
- 设置信号捕抓函数
- 有信号发生,就往socketpair写入一个字节
统一事件源能够工作的一个原因是:多路IO复用函数都是可中断的。即处理完信号后,会从多路IO复用函数中退出,并将errno赋值为EINTR。有些OS的某些系统调用,比如Linux的read,即使被信号终端了,还是会自启动的。即不会从read函数中退出来。
用于信号event的结构体和变量:
在说之前,看一下event_base在信号监听这方面提供了什么的成员。
-
- struct event_base {
-
- const struct eventop *evsigsel;
- struct evsig_info sig;
-
- ...
- struct event_signal_map sigmap;
- ...
- };
-
-
-
- struct evsig_info {
-
- struct event ev_signal;
-
- evutil_socket_t ev_signal_pair[2];
-
- int ev_signal_added;
-
- int ev_n_signals_added;
-
-
-
-
-
-
- #ifdef _EVENT_HAVE_SIGACTION
- struct sigaction **sh_old;
- #else//保存的是捕抓函数的函数指针,又因为是数组。所以是二级指针
- ev_sighandler_t **sh_old;
- #endif
-
- int sh_old_max;
- };
在上面代码中,已经可以看到用于socketpair的ev_signal_pair变量,还有struct event结构体变量ev_signal。那么Libevent是在什么时候创建socketpair以及怎么将socketpair和ev_signal相关联的呢?
初始化:
在前面的博文《 跨平台Reactor接口的实现
》中,有说到Libevent是怎么选择一个多路IO复用函数的。在选定一个多路IO复用函数后,就会调用下面一行代码。
- base->evbase = base->evsel->init(base);
这是初始化代码函数。下面给出poll的init函数。
-
- static void *
- poll_init(struct event_base *base)
- {
- struct pollop *pollop;
-
- if (!(pollop = mm_calloc(1, sizeof(struct pollop))))
- return (NULL);
-
- evsig_init(base);
-
- return (pollop);
- }
可以看到,其调用了evsig_init函数。而正是这个evsig_init函数完成了创建socketpair并将socketpair的一个读端与ev_signal相关联。
-
- int
- evsig_init(struct event_base *base)
- {
-
- if (evutil_socketpair(
- AF_UNIX, SOCK_STREAM, 0, base->sig.ev_signal_pair) == -1) {
- #ifdef WIN32
-
-
- event_sock_warn(-1, "%s: socketpair", __func__);
- #else
- event_sock_err(1, -1, "%s: socketpair", __func__);
- #endif
- return -1;
- }
-
-
- 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]);
-
-
-
- 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;
- }
socketpair的两个端都调用evutil_make_socket_closeonexec,因为不能让子进程可以访问的这个socketpair。因为子进程的访问可能会出现扰乱。比如,子进程往socketpair发送信息,使得父进程的多路IO复用函数误以为信号发生了;父进程确实发生了信号,也往socketpair发送了一个字节,但却被子进程接收了这个字节。父进程没有监听到可读。
在Windows中,并没有直接的可以使用的socketpair API。此时,Libevent就自己实现了一个socketpair。具体可以参考《通用类型和函数》。
在函数的最后可以看到event_base的一个成员evsignal被赋值。evsignal是一个IO复用结构体,而evsigops是专门用于信号处理的 IO复用结构体
变量。定义如下:
-
- static const struct eventop evsigops = {
- "signal",
- NULL,
- evsig_add,
- evsig_del,
- NULL,
- NULL,
- 0, 0, 0
- };
该结构体只有evsig_add和evsig_del这两个函数指针。实际在工作时有这两个函数就足够了。
将信号event加入到event_base:
前面的代码已经完成了“创建socketpair并将socketpair的一个读端于ev_signal相关联”。接下来看其他的工作。假如要对一个绑定了某个信号的event调用event_add函数,那么在event_add的内部会调用event_add_internal函数。而event_add_internal函数又会调用evmap_signal_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;
-
- ...
-
- if (TAILQ_EMPTY(&ctx->events)) {
-
- if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
- == -1)
- return (-1);
- }
-
- return (1);
- }
上面函数的内部调用了IO复用结构体的add函数指针,即调用了evsig_add。现在我们深入evsig_add函数。
- /signal.c文件
- 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);
-
-
-
-
- EVSIGBASE_LOCK();
-
- if (evsig_base != base && evsig_base_n_signals_added) {
- 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();
-
-
- if (_evsig_set_handler(base, (int)evsignal, evsig_handler) == -1) {
- goto err;
- }
-
-
- 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);
- }
从后面的那个if语句可以得知,当sig->ev_signal_added变量为0时(即用户第一次监听一个信号),就会将ev_signal这个event加入到event_base中。从前面的“统一事件源”可以得知,这个ev_signal的作用就是通知event_base,有信号发生了。只需一个event即可完成工作,即使用户要监听多个不同的信号,因为这个event已经和socketpair的读端相关联了。如果要监听多个信号,那么就在信号处理函数中往这个socketpair写入不同的值即可。event_base能监听到可读,并可以从读到的内容可以判断是哪个信号发生了。
从代码中也可得知,Libevent并不会为每一个信号监听创建一个event。它只会创建一个全局的专门用于监听信号的event。这个也是“统一事件源”的工作原理。
设置信号捕抓函数:
evsig_add函数还调用了_evsig_set_handler函数完成设置Libevent内部的信号捕抓函数。
-
- typedef void (*ev_sighandler_t)(int);
-
-
- 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;
-
-
-
- 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;
- }
-
-
-
- sig->sh_old[evsignal] = mm_malloc(sizeof *sig->sh_old[evsignal]);
- if (sig->sh_old[evsignal] == NULL) {
- event_warn("malloc");
- return (-1);
- }
-
-
- #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);
- }
如果看过《UNIX环境高级编程》信号那章的话,上面这段代码很容易看懂。这里就不讲了。
这里我们做一个猜测:当我们对某个信号进行event_new和event_add后,就不应该再次设置该信号的信号捕抓函数。否则event_base将无法监听到信号的发生。下面代码验证这猜测。
- #include<unistd.h>
- #include<stdio.h>
- #include<signal.h>
- #include<event.h>
-
-
- void sig_cb(int fd, short events, void *arg)
- {
- printf("in the sig_cb\n");
- }
-
- void signal_handle(int sig)
- {
- printf("catch the sig %d\n", sig);
- }
-
- int main()
- {
-
- struct event_base *base = event_base_new();
-
- struct event *ev = evsignal_new(base, SIGUSR1, sig_cb, NULL);
- event_add(ev, NULL);
-
- signal(SIGUSR1, signal_handle);
-
- printf("pid = %d\n", getpid());
-
- printf("begin\n");
- event_base_dispatch(base);
- printf("end\n");
-
- return 0;
- }
运行上面代码, 通过在外部给这个进程发生信号的方式。可以看到,event_base确实无法监听到信号了。所有信号都被signal_handle捕抓了。
捕抓信号:
前面的代码中有两个函数并没有讲,分别是信号捕抓函数evsig_handler和调用event_assign时的信号回调函数evsig_cb。
-
- static void __cdecl
- evsig_handler(int sig)
- {
- ...
- 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
-
-
- msg = sig;
- send(evsig_base_fd, (char*)&msg, 1, 0);
-
- ...
- }
从evsig_handler函数的实现可以看到,实现得相当简单。只是将信号对应的值写入到socketpair中。evsig_base_fd是socketpair的写端,这是一个全局变量,在evsig_add函数中被赋值的。
从“统一事件源”的工作原理来看,现在已经完成了对信号的捕抓,已经将该信号的当作IO事件写入到socketpair中了。现在event_base应该已经监听到socketpair可读了,并且会为调用回调函数evsig_cb了。下面看看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];
- struct event_base *base;
-
- base = arg;
-
- memset(&ncaught, 0, sizeof(ncaught));
-
- while (1) {
-
-
-
- n = recv(fd, signals, sizeof(signals), 0);
- 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) {
-
- 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);
- }
该回调函数的作用是读取socketpair的所有数据,并将数据当作信号,再根据信号值调用evmap_signal_active。
有一点要注意,evsig_cb这个回调函数并不是用户为监听一个信号调用event_new时设置的用户回调函数,而是Libevent内部为了处理信号而设置的内部回调函数。累!!
激活信号event:
虽然如此,但是现在的情况是:当有信号发生时,就会调用evmap_signal_active函数。
-
- #define ev_signal_next _ev.ev_signal.ev_signal_next
-
- #define ev_ncalls _ev.ev_signal.ev_ncalls
- #define ev_pncalls _ev.ev_signal.ev_pncalls
-
-
- void
- evmap_signal_active(struct event_base *base, evutil_socket_t sig, int ncalls)
- {
- struct event_signal_map *map = &base->sigmap;
- struct evmap_signal *ctx;
- struct event *ev;
-
-
- GET_SIGNAL_SLOT(ctx, map, sig, evmap_signal);
-
-
- TAILQ_FOREACH(ev, &ctx->events, ev_signal_next)
- event_active_nolock(ev, EV_SIGNAL, ncalls);
- }
-
-
-
- void
- event_active_nolock(struct event *ev, int res, short ncalls)
- {
- struct event_base *base;
-
-
- base = ev->ev_base;
- ev->ev_res = res;
-
-
- if (ev->ev_pri < base->event_running_priority)
- base->event_continue = 1;
-
- if (ev->ev_events & EV_SIGNAL) {
- #ifndef _EVENT_DISABLE_THREAD_SUPPORT
- if (base->current_event == ev && !EVBASE_IN_THREAD(base)) {
- ++base->current_event_waiters;
-
- EVTHREAD_COND_WAIT(base->current_event_cond, base->th_base_lock);
- }
- #endif
- ev->ev_ncalls = ncalls;
- ev->ev_pncalls = NULL;
- }
-
-
- event_queue_insert(base, ev, EVLIST_ACTIVE);
-
- }
通过evmap_signal_active、event_active_nolock和event_queue_insert这三个函数的调用后,就可以把一个event插入到激活队列了。
由于这些函数的执行本身就是在Libevent处理event的回调函数之中的(Libevent正在处理内部的信号处理event)。所以并不需要从event_base_loop里的while循环里面再次执行一次evsel->dispatch(),才能执行到这次信号event。即无需等到下一次处理激活队列,就可以执行该信号event了。分析如下:
首先要明确,现在执行上面三个函数相当于在执行event的回调函数。所以其是运行在event_process_active函数之中的。为什么是在这里,可以参考《Libevent工作流程探究》一文。
-
- static int
- event_process_active(struct event_base *base)
- {
- struct event_list *activeq = NULL;
- int i, c = 0;
-
-
- for (i = 0; i < base->nactivequeues; ++i) {
-
- if (TAILQ_FIRST(&base->activequeues[i]) != NULL) {
- base->event_running_priority = i;
- activeq = &base->activequeues[i];
- c = event_process_active_single_queue(base, activeq);
- }
- }
-
- return c;
- }
-
- static int
- event_process_active_single_queue(struct event_base *base,
- struct event_list *activeq)
- {
- struct event *ev;
-
-
- for (ev = TAILQ_FIRST(activeq); ev; ev = TAILQ_FIRST(activeq)) {
-
- ...
- }
-
- }
从上面的代码可以看到,Libevent在处理内部的那个信号处理event的回调函数时,其实是在event_process_active_single_queue的一个循环里面。因为Libevent内部的信号处理event的优先级最高优先级,并且在前面的将用户信号event插入到队列(即event_queue_insert),在插入到队列的尾部。所以无论用户的这个信号event的优先级是多少,都是在Libevent的内部信号处理event的后面。所以在遍历上面两个函数的里外两个循环时,肯定会执行到用户的信号event。
执行已激活信号event:
现在看看Libevent是怎么处理已激活的信号event的。
-
- static inline void
- event_signal_closure(struct event_base *base, struct event *ev)
- {
- short ncalls;
- int should_break;
-
-
- ncalls = ev->ev_ncalls;
- if (ncalls != 0)
- ev->ev_pncalls = &ncalls;
-
-
-
- EVBASE_RELEASE_LOCK(base, th_base_lock);
-
- while (ncalls) {
- ncalls--;
- ev->ev_ncalls = ncalls;
- if (ncalls == 0)
- ev->ev_pncalls = NULL;
- (*ev->ev_callback)(ev->ev_fd, ev->ev_res, ev->ev_arg);
-
- EVBASE_ACQUIRE_LOCK(base, th_base_lock);
-
- should_break = base->event_break;
- EVBASE_RELEASE_LOCK(base, th_base_lock);
-
- if (should_break) {
- if (ncalls != 0)
- ev->ev_pncalls = NULL;
- return;
- }
- }
- }
可以看到,如果对应的信号发生了多次,那么该信号event的回调函数将被执行多次。