Libuv源码分析 —— 13. 信号

725 阅读9分钟

信号

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检测到; 这些信号将不会触发信号监视器。

数据类型

  • uv_signal_t

    信号句柄类型。

API

  • int uv_signal_start(uv_signal_t* signaluv_signal_cb cb, int signum)

    以给定的回调函数开始句柄,监视给定的信号。

  • 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 里的回调函数执行。