7.1 信号的概念和实现原理

信号是进程间通信的一种简单的方式,我们首先了解一下信号的概念和在操作系统中的实现原理。在操作系统内核的实现中,每个进程对应一个task_struct结构体(PCB),PCB中有一个字段记录了进程收到的信号(每一个比特代表一种信号)和信号对应的处理函数。这个和订阅者/发布者模式非常相似,我们看一下PCB中信号对应的数据结构。

    struct task_struct {  
        // 收到的信号  
        long signal;  
        // 处理信号过程中屏蔽的信息  
        long blocked;  
        // 信号对应的处理函数  
        struct sigaction sigaction[32];  
           ...  
    };  

    struct sigaction {  
        // 信号处理函数  
        void (*sa_handler)(int);  
        // 处理信号时屏蔽哪些信息,和PCB的block字段对应  
        sigset_t sa_mask;  
        // 一些标记,比如处理函数只执行一次,类似events模块的once  
        int sa_flags;  
        // 清除调用栈信息,glibc使用  
        void (*sa_restorer)(void);  
    };  

Linux下支持多种信号,进程收到信号时,操作系统提供了默认处理,我们也可以显式注册处理信号的函数,但是有些信号会导致进程退出,这是我们无法控制的。我们来看一下在Linux下信号使用的例子。

    #include <stdio.h>  
    #include <unistd.h>  
    #include <stdlib.h>  
    #include <signal.h>  

    void handler(int);  

    int main()  
    {  
       signal(SIGINT, handler);  
       while(1);  
       return(0);  
    }  

    void sighandler(int signum)  
    {  
       printf("收到信号%d", signum);  
    }  

我们注册了一个信号对应的处理函数,然后进入while循环保证进程不会退出,这时候,如果我们给这个进程发送一个SIGINT信号(ctrl+c或者kill -2 pid)。则进程会执行对应的回调,然后输出:收到信号2。了解了信号的基本原理后,我们看一下Libuv中关于信号的设计和实现。

7.2 Libuv信号处理的设计思想

由于操作系统实现的限制,我们无法给一个信号注册多个处理函数,对于同一个信号,如果我们调用操作系统接口多次,后面的就会覆盖前面设置的值。想要实现一个信号被多个函数处理,我们只能在操作系统之上再封装一层,Libuv正是这样做的。Libuv中关于信号处理的封装和订阅者/发布者模式很相似。用户调用Libuv的接口注册信号处理函数,Libuv再向操作系统注册对应的处理函数,等待操作系统收到信号时,会触发Libuv的回调,Libuv的回调会通过管道通知事件循环收到的信号和对应的上下文,接着事件循环在Poll IO阶段就会处理收到所有信号以及对应的处理函数。整体架构如图7-1所示

图7-1

下面我们具体分析Libuv中信号处理的实现。

7.3 通信机制的实现

当进程收到信号的时候,信号处理函数需要通知Libuv事件循环,从而在事件循环中执行对应的回调,实现函数是uv__signal_loop_once_init,我们看一下uv__signal_loop_once_init的逻辑。

    static int uv__signal_loop_once_init(uv_loop_t* loop) { 
      /* 
            申请一个管道用于和事件循环通信,通知事件循环是否收到信号,
            并设置非阻塞标记  
        */
      uv__make_pipe(loop->signal_pipefd, UV__F_NONBLOCK); 
      /* 
          设置信号IO观察者的处理函数和文件描述符, 
          Libuv在Poll IO时,发现管道读端loop->signal_pipefd[0]可读, 
          则执行uv__signal_event 
        */  
      uv__io_init(&loop->signal_io_watcher,  
                  uv__signal_event,  
                  loop->signal_pipefd[0]);  
      /* 
          插入Libuv的IO观察者队列,并注册感兴趣的事件为可读
        */  
      uv__io_start(loop, &loop->signal_io_watcher, POLLIN);  

      return 0; 
    } 

uvsignal_loop_once_init首先申请一个管道,用于通知事件循环是否收到信号。然后往Libuv的IO观察者队列注册一个观察者,Libuv在Poll IO阶段会把观察者加到epoll中。IO观察者里保存了管道读端的文件描述符loop->signal_pipefd[0]和回调函数uvsignal_event。uv__signal_event是收到任意信号时的回调,它会继续根据收到的信号进行逻辑分发。执行完的架构如图7-2所示。

图7-2

7.4 信号结构体的初始化

Libuv中信号使用uv_signal_t表示。

    int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle) { 
      // 申请和Libuv的通信管道并且注册IO观察者  
      uv__signal_loop_once_init(loop);  
      uv__handle_init(loop, (uv_handle_t*) handle, UV_SIGNAL);  
      handle->signum = 0;  
      handle->caught_signals = 0;  
      handle->dispatched_signals = 0;  

      return 0;  
    }   

上面的代码的逻辑比较简单,只是初始化uv_signal_t结构体的一些字段。

7.5 信号处理的注册

我们可以通过uv_signal_start注册一个信号处理函数。我们看看这个函数的逻辑

    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;  
      // 注册过了,重新设置处理函数就行  
      if (signum == handle->signum) {  
        handle->signal_cb = signal_cb;  
        return 0;  
      }  
      // 这个handle之前已经设置了其它信号和处理函数,则先解除  
      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) {  
          uv__signal_unlock_and_unblock(&saved_sigmask);  
          return err;  
        }  
      }  
      // 记录感兴趣的信号  
      handle->signum = signum;  
      // 只处理该信号一次  
      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;  
      uv__handle_start(handle);  

      return 0;  
    } 

上面的代码比较多,大致的逻辑如下. 1 判断是否需要向操作系统注册一个信号的处理函数。主要是调用操作系统的函数来处理的,代码如下

    // 给当前进程注册信号处理函数,会覆盖之前设置的signum的处理函数  
    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,我们一会会分析uv__signal_handler的实现。
2进程注册的信号和回调是在一棵红黑树管理的,每次注册的时候会往红黑树插入一个节点。Libuv用黑红树维护信号的上下文,插入的规则是根据信号的大小和flags等信息。 RB_INSERT实现了往红黑树插入一个节点,红黑树中的节点是父节点的值比左孩子大,比右孩子小的。执行完RB_INSERT后的架构如图7-3所示。

图7-3

我们看到,当我们每次插入不同的信号的时候,Libuv会在操作系统和红黑树中修改对应的数据结构。那么如果我们插入重复的信号呢?刚才我们已经分析过,插入重复的信号时,如果在操作系统注册过,并且当前插入的信号flags是one shot,而之前是非one shot时,Libuv会调用操作系统的接口去修改配置。那么对于红黑树来说,插入重复信号会如何处理呢?从刚才RB_INSERT的代码中我们看到每次插入红黑树时,红黑树会先判断是否存在相同值的节点,如果是的话直接返回,不进行插入。这么看起来我们无法给一个信号注册多个处理函数,但其实是可以的,重点在比较大小的函数。我们看看该函数的实现。

    static int uv__signal_compare(uv_signal_t* w1, uv_signal_t* w2) {  
      int f1;  
      int f2;  

      // 返回信号值大的  
      if (w1->signum < w2->signum) return -1;  
      if (w1->signum > w2->signum) return 1;  

      // 设置了UV_SIGNAL_ONE_SHOT的大  
      f1 = w1->flags & UV_SIGNAL_ONE_SHOT;  
      f2 = w2->flags & UV_SIGNAL_ONE_SHOT;  
      if (f1 < f2) return -1;  
      if (f1 > f2) return 1;  

      // 地址大的值就大  
      if (w1->loop < w2->loop) return -1;  
      if (w1->loop > w2->loop) return 1;  

      if (w1 < w2) return -1;  
      if (w1 > w2) return 1;  

      return 0;  
    }  

我们看到Libuv比较的不仅是信号的大小,在信号一样的情况下,Libuv还会比较其它的因子,除非两个uv_signal_t指针指向的是同一个uv_signal_t结构体,否则它们是不会被认为重复的,所以红黑树中会存着信号一样的节点。假设我们按照1(flags为one shot),2(flags为非one shot),3(flags为one shot)的顺序插入红黑树,并且节点3比节点1的地址大。所形成的结构如图7-4所示。

图7-4

7.6 信号的处理

我们上一节已经分析过,不管注册什么信号,它的处理函数都是这个uvsignal_handler函数。我们自己的业务回调函数,是保存在handle里的。而Libuv维护了一棵红黑树,记录了每个handle注册的信号和回调函数,那么当任意信号到来的时候。uv__signal_handler就会被调用。下面我们看看uvsignal_handler函数。

    /* 
      信号处理函数,signum为收到的信号, 
      每个子进程收到信号的时候都由该函数处理, 
      然后通过管道通知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;  
      }  
      // 找到该信号对应的所有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;  
        do {  
          // 通知Libuv,哪些handle需要处理该信号,
                 在Poll IO阶段处理  
          r = write(handle->loop->signal_pipefd[1], 
                            &msg, 
                            sizeof msg);  
        } while (r == -1 && errno == EINTR);  
        // 该handle收到信号的次数  
        if (r != -1)  
          handle->caught_signals++;  
      }  

      uv__signal_unlock();  
      errno = saved_errno;  
    }  

uv__signal_handler函数会调用uvsignal_first_handle遍历红黑树,找到注册了该信号的所有handle,我们看一下uvsignal_first_handle的实现。

    static uv_signal_t* uv__signal_first_handle(int signum) {  
      uv_signal_t lookup;  
      uv_signal_t* handle;  

      lookup.signum = signum;  
      lookup.flags = 0;  
      lookup.loop = NULL;  

      handle = RB_NFIND(uv__signal_tree_s, 
                         &uv__signal_tree, 
                         &lookup);  

      if (handle != NULL && handle->signum == signum)  
        return handle;  
      return NULL;  
    }  

uv__signal_first_handle函数通过RB_NFIND实现红黑树的查找,RB_NFIND是一个宏。

    #define RB_NFIND(name, x, y)    name##_RB_NFIND(x, y)  

我们看看name##_RB_NFIND即uv__signal_tree_s_RB_NFIND的实现

    static struct uv_signal_t * uv__signal_tree_s_RB_NFIND(struct uv__signal_tree_s *head, struct uv_signal_t *elm)                            
    {                                    
      struct uv_signal_t *tmp = RB_ROOT(head);    
      struct uv_signal_t *res = NULL;    
      int comp;              
      while (tmp) {    
        comp = cmp(elm, tmp);     
         /* 
           elm小于当前节点则往左子树找,大于则往右子树找,
          等于则返回           
         */
        if (comp < 0) {            
          // 记录父节点
          res = tmp;  
          tmp = RB_LEFT(tmp, field);    
        }           
        else if (comp > 0)    
          tmp = RB_RIGHT(tmp, field); 
        else         
          return (tmp);  
      }             
      return (res); 
    }     

uv__signal_tree_s_RB_NFIND的逻辑就是根据红黑树的特点进行搜索,这里的重点是cmp函数。刚才我们已经分析过cmp的逻辑。这里会首先查找没有设置one shot标记的handle(因为它的值小),然后再查找设置了one shot的handle,一旦遇到设置了one shot的handle,则说明后面被匹配的handle也是设置了one shot标记的。每次找到一个handle,就会封装一个msg写入管道(即和Libuv通信的管道)。信号的处理就完成了。接下来在Libuv的Poll IO阶段才做真正的处理。我们知道在Poll IO阶段。epoll会检测到管道loop->signal_pipefd[0]可读,然后会执行uv__signal_event函数。我们看看这个函数的代码。

    // 如果收到信号,Libuv Poll IO阶段,会执行该函数  
    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 {  
        // 读出所有的uv__signal_msg_t  
        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 (bytes > 0)  
            continue;  
          return;  
        }  
        if (r == -1)  
          abort();  
        bytes += r;  
        /*
              根据收到的字节数算出有多少个uv__signal_msg_t结构体,
              从而算出结束位置
            */ 
        end=(bytes/sizeof(uv__signal_msg_t))*sizeof(uv__signal_msg_t);
          // 循环处理每一个msg
        for (i = 0; i < end; i += sizeof(uv__signal_msg_t)) {
          msg = (uv__signal_msg_t*) (buf + i); 
                // 取出上下文 
          handle = msg->handle;  
          // 收到的信号和handle感兴趣的信号一致,执行回调  
          if (msg->signum == handle->signum) {    
            handle->signal_cb(handle, handle->signum);  
          }  
          // 处理信号个数,和收到的个数对应  
          handle->dispatched_signals++;  
          // 只执行一次,恢复系统默认的处理函数  
          if (handle->flags & UV_SIGNAL_ONE_SHOT)  
            uv__signal_stop(handle);  
          /* 
                  处理完所有收到的信号才能关闭uv_signal_t,
                  见uv_close或uv__signal_close 
                */ 
          if ((handle->flags & UV_HANDLE_CLOSING) &&  
            (handle->caught_signals==handle->dispatched_signals))          
               {  
            uv__make_close_pending((uv_handle_t*) handle);  
          }  
        }  
        bytes -= end; 
        if (bytes) {  
          memmove(buf, buf + end, bytes);  
          continue;  
        }  
      } while (end == sizeof buf);  
    }  

uv__signal_event函数的逻辑如下
1 读出管道里的数据,计算出msg的个数。
2 遍历收到的数据,解析出一个个msg。
3 从msg中取出上下文(handle和信号),执行上层回调。
4 如果handle设置了one shot则需要执行uv__signal_stop(我们接下来分析)。
5 如果handle设置了closing标记,则判断所有收到的信号是否已经处理完。即收到的个数和处理的个数是否一致。需要处理完所有收到的信号才能关闭uv_signal_t。

7.7 取消/关闭信号处理

当一个信号对应的handle设置了one shot标记,在收到信号并且执行完回调后,Libuv会调用uvsignal_stop关闭该handle并且从红黑树中移除该handle。另外我们也可以显式地调用uv_close(会调用uvsignal_stop)关闭或取消信号的处理。下面我们看看uv__signal_stop的实现。

    static void uv__signal_stop(uv_signal_t* handle) {  
      uv_signal_t* removed_handle;  
      sigset_t saved_sigmask;  
      uv_signal_t* first_handle;  
      int rem_oneshot;  
      int first_oneshot;  
      int ret;  

      /* If the watcher wasn't started, this is a no-op. */  
      // 没有注册过信号,则不需要处理  
      if (handle->signum == 0)  
        return;  
      // 屏蔽所有信号  
      uv__signal_block_and_lock(&saved_sigmask);  
      // 移出红黑树  
      removed_handle = RB_REMOVE(uv__signal_tree_s, &uv__signal_tree, handle);  
      // 判断该信号是否还有对应的handle  
      first_handle = uv__signal_first_handle(handle->signum);  
      // 为空说明没有handle会处理该信号了,解除该信号的设置  
      if (first_handle == NULL) {  
        uv__signal_unregister_handler(handle->signum);  
      } else {  
        // 被处理的handle是否设置了one shot  
        rem_oneshot = handle->flags & UV_SIGNAL_ONE_SHOT;  
        /*
          剩下的第一个handle是否设置了one shot,
          如果是则说明该信号对应的所有剩下的handle都是one shot  
        */ 
        first_oneshot = first_handle->flags & UV_SIGNAL_ONE_SHOT;  
        /* 
          被移除的handle没有设置oneshot但是当前的第一个handle设置了
           one shot,则需要修改该信号处理函数为one shot,防止收到多次信
           号,执行多次回调 
        */  
        if (first_oneshot && !rem_oneshot) {  
          ret = uv__signal_register_handler(handle->signum, 1);  
          assert(ret == 0);  
        }  
      }  

      uv__signal_unlock_and_unblock(&saved_sigmask);  

      handle->signum = 0;  
      uv__handle_stop(handle);  
    }  

7.8 信号在Node.js中的使用

分析完Libuv的实现后,我们看看Node.js上层是如何使用信号的,首先我们看一下C++层关于信号模块的实现。

    static void Initialize(Local<Object> target,  
                             Local<Value> unused,  
                             Local<Context> context,  
                             void* priv) {  
        Environment* env = Environment::GetCurrent(context);  
        Local<FunctionTemplate> constructor = env->NewFunctionTemplate(New);  
        constructor->InstanceTemplate()->SetInternalFieldCount(1);  
        // 导出的类名  
        Local<String> signalString =  
            FIXED_ONE_BYTE_STRING(env->isolate(), "Signal");  
        constructor->SetClassName(signalString);  
        constructor->Inherit(HandleWrap::GetConstructorTemplate(env));  
        // 给Signal创建的对象注入两个函数  
        env->SetProtoMethod(constructor, "start", Start);  
        env->SetProtoMethod(constructor, "stop", Stop);  

        target->Set(env->context(), signalString,  
                    constructor->GetFunction(env->context()).ToLocalChecked()).Check();  
      }  

当我们在JS中new Signal的时候,首先会创建一个C++对象,然后作为入参执行New函数。

    static void New(const FunctionCallbackInfo<Value>& args) {  
        CHECK(args.IsConstructCall());  
        Environment* env = Environment::GetCurrent(args);  
        new SignalWrap(env, args.This());  
    }  

当我们在JS层操作Signal实例的时候,就会执行C++层对应的方法。主要的方法是注册和删除信号。

    static void Start(const FunctionCallbackInfo<Value>& args) {  
        SignalWrap* wrap;  
        ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());  
        Environment* env = wrap->env();  
        int signum;  
        if (!args[0]->Int32Value(env->context()).To(&signum)) return;  
        int err = uv_signal_start(  
            &wrap->handle_,  
            // 信号产生时执行的回调  
            [](uv_signal_t* handle, int signum) {  
              SignalWrap* wrap = ContainerOf(&SignalWrap::handle_, 
                                                 handle);  
              Environment* env = wrap->env();  
              HandleScope handle_scope(env->isolate());  
              Context::Scope context_scope(env->context());  
              Local<Value> arg = Integer::New(env->isolate(), 
                                                  signum);  
              // 触发JS层onsignal函数  
              wrap->MakeCallback(env->onsignal_string(), 1, &arg);  
            },  
            signum);  

        if (err == 0) {  
          CHECK(!wrap->active_);  
          wrap->active_ = true;  
          Mutex::ScopedLock lock(handled_signals_mutex);  
          handled_signals[signum]++;  
        }  

        args.GetReturnValue().Set(err);  
      }  

      static void Stop(const FunctionCallbackInfo<Value>& args) {
        SignalWrap* wrap;
        ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());

        if (wrap->active_)  {
          wrap->active_ = false;
          DecreaseSignalHandlerCount(wrap->handle_.signum);
        }

        int err = uv_signal_stop(&wrap->handle_);
        args.GetReturnValue().Set(err);
      }

接着我们看在JS层如何使用。Node.js在初始化的时候,在is_main_thread.js中执行了。

    process.on('newListener', startListeningIfSignal);  
    process.on('removeListener', stopListeningIfSignal)  

newListener和removeListener事件在注册和删除事件的时候都会被触发。我们看一下这两个函数的实现

    /* 
     { 
      SIGINT: 2, 
      ... 
     } 
    */  
    const { signals } = internalBinding('constants').os;  

    let Signal;  
    const signalWraps = new Map();  

    function isSignal(event) {  
      return typeof event === 'string' && signals[event] !== undefined;  
    }  

    function startListeningIfSignal(type) {  
      if (isSignal(type) && !signalWraps.has(type)) {  
        if (Signal === undefined)  
          Signal = internalBinding('signal_wrap').Signal;  
        const wrap = new Signal();  
        // 不影响事件循环的退出  
        wrap.unref();  
        // 挂载信号处理函数  
        wrap.onsignal = process.emit.bind(process, type, type);  
        // 通过字符拿到数字  
        const signum = signals[type];  
        // 注册信号  
        const err = wrap.start(signum);  
        if (err) {  
          wrap.close();  
          throw errnoException(err, 'uv_signal_start');  
        }  
        // 该信号已经注册,不需要往底层再注册了  
        signalWraps.set(type, wrap);  
      }  
    }  

startListeningIfSignal函数的逻辑分为一下几个 1 判断该信号是否注册过了,如果注册过了则不需要再注册。Libuv本身支持在同一个信号上注册多个处理函数,Node.js的JS层也做了这个处理。 2 调用unref,信号的注册不应该影响事件循环的退出 3 挂载事件处理函数,当信号触发的时候,执行对应的处理函数(一个或多个)。 4 往底层注册信号并设置该信号已经注册的标记 我们再来看一下stopListeningIfSignal。

    function stopListeningIfSignal(type) {  
      const wrap = signalWraps.get(type);  
      if (wrap !== undefined && process.listenerCount(type) === 0) { 
        wrap.close();  
        signalWraps.delete(type);  
      }  
    }  

只有当信号被注册过并且事件处理函数个数为0,才做真正的删除。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8