信号
uv_signal_t
--- 信号句柄
信号句柄在按事件循环的基础上实现了Unix风格的信号处理。
在Windows上模拟了一些信号的接收:
- SIGINT 通常在用户按 CTRL+C 时被发送。 然而,如同在Unix上,当终端原始模式开启时这无法保证。
- SIGBREAK 在用户按 CTRL + BREAK 时被发送。
- SIGHUP 在用户关闭终端窗口时被生成。 在SIGHUP时给予程序大约10秒执行清理。 在那之后Windows将会无条件地终止程序。
- 对其他类型的信号的监视器能被成功地创建,但是这些信号永远不会被接收到。 这些信号是: SIGILL 、 SIGABRT 、 SIGFPE 、 SIGSEGV 、 SIGTERM 和 SIGKILL 。
- 通过编程方式调用 raise() 或 abort() 发出一个信号不会被libuv检测到; 这些信号将不会触发信号监视器。
数据类型
-
void
(*uv_signal_cb)
(uv_signal_t* handle, int signum)传递给
uv_signal_start()
的回调函数的类型定义。
API
-
int
uv_signal_init
(uv_loop_t* loop, uv_signal_t* signal)初始化句柄。
-
int
uv_signal_start
(uv_signal_t* signal, uv_signal_cb cb, int signum)以给定的回调函数开始句柄,监视给定的信号。
-
int
uv_signal_start_oneshot
(uv_signal_t* signal, uv_signal_cb cb, int signum)与
uv_signal_start()
同样的功能,但是信号句柄在接受到信号的时刻被重置。
-
int
uv_signal_stop
(uv_signal_t* signal)停止句柄,回调函数将不会再被调用。
example
- 例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <uv.h> uv_loop_t* create_loop() { uv_loop_t *loop = malloc(sizeof(uv_loop_t)); if (loop) { uv_loop_init(loop); } return loop; } void signal_handler(uv_signal_t *handle, int signum) { printf("Signal received: %d\n", signum); uv_signal_stop(handle); } // two signal handlers in one loop void thread1_worker(void *userp) { uv_loop_t *loop1 = create_loop(); uv_signal_t sig1a, sig1b; uv_signal_init(loop1, &sig1a); uv_signal_start(&sig1a, signal_handler, SIGUSR1); uv_signal_init(loop1, &sig1b); uv_signal_start(&sig1b, signal_handler, SIGUSR1); uv_run(loop1, UV_RUN_DEFAULT); } // two signal handlers, each in its own loop void thread2_worker(void *userp) { uv_loop_t *loop2 = create_loop(); uv_loop_t *loop3 = create_loop(); uv_signal_t sig2; uv_signal_init(loop2, &sig2); uv_signal_start(&sig2, signal_handler, SIGUSR1); uv_signal_t sig3; uv_signal_init(loop3, &sig3); uv_signal_start(&sig3, signal_handler, SIGUSR1); while (uv_run(loop2, UV_RUN_NOWAIT) || uv_run(loop3, UV_RUN_NOWAIT)) { } } /* 使用uv_signal_init初始化handle(uv_signal_t),然后将它与loop关联。为了使用 handle监听特定的信号,使用uv_signal_start()函数。每一个handle只能与一个信号 关联,后续的uv_signal_start会覆盖前面的关联。使用uv_signal_stop终止监听 uv_run(loop, UV_RUN_NOWAIT)和uv_run(loop, UV_RUN_ONCE)非常像,因为它们都只 处理一个事件。但是不同在于,UV_RUN_ONCE会在没有任务的时候阻塞,但是UV_RUN_NOWAIT 会立刻返回。我们使用NOWAIT,这样才使得一个loop不会因为另外一个loop没有要处理的事 件而挨饿 当向进程发送SIGUSR1,你会发现signal_handler函数被激发了4次,每次都对应一个uv_signal_t。 然后signal_handler调用uv_signal_stop终止了每一个uv_signal_t,最终程序退出。对每个handler 函数来说,任务的分配很重要。一个使用了多个event-loop的服务器程序,只要简单地给每一个进程添 加信号SIGINT监视器,就可以保证程序在中断退出前,数据能够安全地保存 */ int main() { printf("PID %d\n", getpid()); uv_thread_t thread1, thread2; uv_thread_create(&thread1, thread1_worker, 0); uv_thread_create(&thread2, thread2_worker, 0); uv_thread_join(&thread1); uv_thread_join(&thread2); return 0; } /* 运行结果: PID 52570 之后终端输入 kill -s SIGUSR1 52570,结果 PID 52570 Signal received: 10 Signal received: 10 Signal received: 10 Signal received: 10 */
源码解析
uv_signal_s
- 源码
struct uv_signal_s { // 句柄[handle]相关参数 // uv_handle_t void* data; uv_loop_t* loop; uv_handle_type type; uv_close_cb close_cb; void* handle_queue[2]; union { int fd; void* reserved[4]; } u; uv_handle_t* next_closing; unsigned int flags; uv_signal_cb signal_cb; int signum; /* 红黑树的节点 */ struct { \ struct uv_signal_s* rbe_left; \ struct uv_signal_s* rbe_right; \ struct uv_signal_s* rbe_parent; \ int rbe_color; \ } tree_entry; \ /* 分别记录了触发信号的次数与处理信号的次数 */ unsigned int caught_signals; \ unsigned int dispatched_signals; }
libuv 初始化
- libuv 初始化的时候会初始化信号处理相关的逻辑
- 代码
// 初始化信号 uv__signal_global_once_init(); err = uv_signal_init(loop, &loop->child_watcher); if (err) goto fail_signal_init; uv__handle_unref(&loop->child_watcher); loop->child_watcher.flags |= UV_HANDLE_INTERNAL;
uv__signal_global_once_init
- 申请了一个用于互斥控制的管道,然后往管道里写数据。后面就可以使用 lock 和 unlock 进行加锁解锁
- 源码
void uv__signal_global_once_init(void) { uv_once(&uv__signal_global_init_guard, uv__signal_global_init); } static void uv__signal_global_init(void) { if (uv__signal_lock_pipefd[0] == -1) // 注册 fork 之后,在子进程执行的函数 if (pthread_atfork(NULL, NULL, &uv__signal_global_reinit)) abort(); uv__signal_global_reinit(); } static void uv__signal_global_reinit(void) { // 清除原来的(如果有的话) uv__signal_cleanup(); // 新建一个管道用于互斥控制 if (uv__make_pipe(uv__signal_lock_pipefd, 0)) abort(); // 先往管道写入数据,即解锁。后续才能顺利 lock,unlock 配对使用 if (uv__signal_unlock()) abort(); }
uv_signal_init
- 源码
// 初始化信号句柄,将signal handle绑定到指定的loop事件循环中 /* libuv申请一个管道,用于其他进程(libuv进程或fork出来的进程)和libuv进程通信。然后往libuv的io观察者队列注册一个观察者, 这其实就是观察这个管道是否可读,libuv在轮询I/O的阶段会把观察者加到epoll中。io观察者里保存了管道读端的文件描述符loop->signal_pipefd[0] 和回调函数uv__signal_event */ int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle) { int err; /* 初始化loop,它只会被初始化一次 */ err = uv__signal_loop_once_init(loop); if (err) return err; /* 初始化handle的类型,并且插入loop的handle队列,因为所有的handle都会被放到该队列管理 */ uv__handle_init(loop, (uv_handle_t*) handle, UV_SIGNAL); handle->signum = 0; handle->caught_signals = 0; handle->dispatched_signals = 0; return 0; } /* 此代码主要的工作有两个 1 申请一个管道,用于其他进程(libuv 进程或 fork 出来的进程)和 libuv 进程通信。 然后往 libuv 的 io 观察者队列注册一个观察者,libuv 在 poll io 阶段会把观察者加到 epoll 中。io 观察者里保存了管道读端的文件描述符 loop->signal_pipefd[0]和回调函 数 uv__signal_event。uv__signal_event 是任意信号触发时的回调,他会继续根据触 发的信号进行逻辑分发。 2 初始化信号信号 handle 的字段。 */ static int uv__signal_loop_once_init(uv_loop_t* loop) { int err; /* 如果已经初始化则返回 */ if (loop->signal_pipefd[0] != -1) return 0; // 申请一个管道,用于其他进程和 libuv 主进程通信,并设置非阻塞标记 err = uv__make_pipe(loop->signal_pipefd, UV_NONBLOCK_PIPE); if (err) return err; /* 置信号io观察者的处理函数和文件描述符,libuv在循环I/O的时候, 如果发现管道读端loop->signal_pipefd[0]可读,则执行对应的回调函数uv__signal_event */ uv__io_init(&loop->signal_io_watcher, uv__signal_event, loop->signal_pipefd[0]); /* 插入libuv的signal io观察者队列,当管道可读的时候,执行uv__signal_event */ uv__io_start(loop, &loop->signal_io_watcher, POLLIN); return 0; }
uv_signal_start
- libuv 用黑红树维护信号数据,插入的规则是根据信号的大小和 flags 等信息
- 通过 uv_signal_start 注册一个信号处理函数。比如 fork 一个子进程时。libuv 会调 uv_signal_start 设置对 SIGCHLD 信号的处理函数为 uv__chld,主进程在收到这个信 号的时候,会执行 uv_chld
int uv_signal_start(uv_signal_t* handle, uv_signal_cb signal_cb, int signum) { return uv__signal_start(handle, signal_cb, signum, 0); } static int uv__signal_start(uv_signal_t* handle, uv_signal_cb signal_cb, int signum, int oneshot) { sigset_t saved_sigmask; int err; uv_signal_t* first_handle; assert(!uv__is_closing(handle)); /* 如果用户提供的 signum == 0,则返回错误。 */ if (signum == 0) return UV_EINVAL; /* 这个信号已经注册过了,重新设置回调处理函数就行。 */ if (signum == handle->signum) { handle->signal_cb = signal_cb; return 0; } /* 如果信号处理程序已经处于活动状态,请先停止它。 */ if (handle->signum != 0) { uv__signal_stop(handle); } /* 暂时屏蔽所有信号 */ uv__signal_block_and_lock(&saved_sigmask); /* 注册了该信号的第一个 handle, 优先返回设置了 UV_SIGNAL_ONE_SHOT flag 的, 见 compare 函数 */ first_handle = uv__signal_first_handle(signum); /* 1 之前没有注册过该信号的处理函数则直接设置 2 之前设置过,但是是 one shot,但是现在需要 设置的规则不是 one shot,需要修改。否则第 二次不会不会触发。因为一个信号只能对应一 个信号处理函数,所以,以规则宽的为准备,在回调 里再根据 flags 判断是不是真的需要执行 3 如果注册过信号和处理函数,则直接插入红黑树就行。 */ if (first_handle == NULL || (!oneshot && (first_handle->flags & UV_SIGNAL_ONE_SHOT))) { // 注册信号和处理函数 err = uv__signal_register_handler(signum, oneshot); if (err) { /* Registering the signal handler failed. Must be an invalid signal. */ /* 注册信号失败 */ uv__signal_unlock_and_unblock(&saved_sigmask); return err; } } // 记录感兴趣的信号 handle->signum = signum; /* 设置UV_SIGNAL_ONE_SHOT标记,表示libuv只响应一次信号 */ if (oneshot) handle->flags |= UV_SIGNAL_ONE_SHOT; /* 插入红黑树 */ RB_INSERT(uv__signal_tree_s, &uv__signal_tree, handle); /* 接触屏蔽信号 */ uv__signal_unlock_and_unblock(&saved_sigmask); // 信号触发时的业务层回调 handle->signal_cb = signal_cb; /* 设置handle的标志UV_HANDLE_ACTIVE,表示处于活跃状态 */ uv__handle_start(handle); return 0; }
uv__signal_register_handler
- 给进程注册一个信号和信号处理函数。主要是调用操作系统的函数来处理的
static int uv__signal_register_handler(int signum, int oneshot) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); // 全置一,说明收到 signum 信号的时候,暂时屏蔽其他信号 if (sigfillset(&sa.sa_mask)) abort(); // 所有信号都由该函数处理 sa.sa_handler = uv__signal_handler; sa.sa_flags = SA_RESTART; // 设置了 oneshot,说明信号处理函数只执行一次,然后被恢复为系统的默认处理函数 if (oneshot) sa.sa_flags |= SA_RESETHAND; // 注册 if (sigaction(signum, &sa, NULL)) return UV__ERR(errno); return 0; }
uv__signal_handler
- 该函数遍历红黑树,找到注册了该信号的handle,然后封装一个msg写入管道(即libuv的通信管道)。信号的通知处理就完成了
- 源码
static void uv__signal_handler(int signum) { uv__signal_msg_t msg; uv_signal_t* handle; int saved_errno; saved_errno = errno; memset(&msg, 0, sizeof msg); // 保持上一个系统调用的错误码 if (uv__signal_lock()) { errno = saved_errno; return; } /* 获取signal handle */ for (handle = uv__signal_first_handle(signum); handle != NULL && handle->signum == signum; handle = RB_NEXT(uv__signal_tree_s, &uv__signal_tree, handle)) { int r; msg.signum = signum; msg.handle = handle; /* 往signal_pipefd管道写入数据,就是通知libuv,拿些signal handle需要处理信号,这是在事件循环中处理的 */ do { r = write(handle->loop->signal_pipefd[1], &msg, sizeof msg); } while (r == -1 && errno == EINTR); assert(r == sizeof msg || (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))); /* 记录该signal handle收到信号的次数 */ if (r != -1) handle->caught_signals++; } uv__signal_unlock(); errno = saved_errno; }
uv__signal_event
- 在 poll io 阶段。epoll 会检测到管道 loop->signal_pipefd[0] 可读, 然后会执行 uv__signal_event 函数
- 源码
/* 在信号通知完成后,事件循环中管道读取数据段有消息到达,此时事件循环将接收到消息,接下来在libuv的poll io阶段才做真正的处理。 从uv__io_init()函数的处理过程得知,它把管道的读取端loop->signal_pipefd[0]看作是一个io观察者,在poll io阶段,epoll会检 测到管道loop->signal_pipefd[0]是否可读,如果可读,然后会执行uv__signal_event()函数。在这个uv__signal_event()函数中, libuv将从管道读取刚才写入的一个个msg,从msg中取出对应的handle,然后执行里面保存的回调函数 */ static void uv__signal_event(uv_loop_t* loop, uv__io_t* w, unsigned int events) { uv__signal_msg_t* msg; uv_signal_t* handle; char buf[sizeof(uv__signal_msg_t) * 32]; size_t bytes, end, i; int r; bytes = 0; end = 0; /* 读取管道里的消息,处理所有的信号消息 */ do { r = read(loop->signal_pipefd[0], buf + bytes, sizeof(buf) - bytes); if (r == -1 && errno == EINTR) continue; if (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { /* If there are bytes in the buffer already (which really is extremely * unlikely if possible at all) we can't exit the function here. We'll * spin until more bytes are read instead. */ if (bytes > 0) continue; /* Otherwise, there was nothing there. */ return; } /* Other errors really should never happen. */ if (r == -1) abort(); bytes += r; /* `end` is rounded down to a multiple of sizeof(uv__signal_msg_t). */ end = (bytes / sizeof(uv__signal_msg_t)) * sizeof(uv__signal_msg_t); for (i = 0; i < end; i += sizeof(uv__signal_msg_t)) { msg = (uv__signal_msg_t*) (buf + i); handle = msg->handle; /* 如果收到的信号与预期的信号是一致的,则执行回调函数 */ if (msg->signum == handle->signum) { assert(!(handle->flags & UV_HANDLE_CLOSING)); /* signal 回调函数 */ handle->signal_cb(handle, handle->signum); } /* 记录处理的信号个数 */ handle->dispatched_signals++; /* 只响应一次,需要回复系统默认的处理函数 */ if (handle->flags & UV_SIGNAL_ONE_SHOT) uv__signal_stop(handle); } bytes -= end; // 处理完了关闭 if (bytes) { memmove(buf, buf + end, bytes); continue; } } while (end == sizeof buf); }
流程
1 libuv 初始化的时候,申请一个管道,用于互斥控制,然后执行往里面写一个数据, 保存后续的 lock 和 unlock 可以顺利执行。
2 执行 uv_signal_init 的时候,初始化 handle 的字段。如果是第一次调用,则申请一 个管道,然后把管道的读端 fd 和回调封装成一个观察者 oi,插入 libuv 的观察者队列。 libuv 会在 poll io 阶段往 epoll 里插入。
3 执 行 uv_signal_start 的时候,给进程注册一个信号和处理函数(固定是 uv__signal_handler)。往红黑树插入一个节点,或者修改里面的节点。
4 如果收到信号,在 uv__signal_handler 函数中会往管道(和 libuv 通信的)写入数 据,即哪些 handle 注册的信号触发了。
5 在 libuv 的 poll io 阶段,从管道读端读出数据,遍历数据,是一个个 msg,取出 msg 里的 handle,然后取出 handle 里的回调函数执行。