Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >libuv源码解析之信号处理

libuv源码解析之信号处理

作者头像
theanarkh
发布于 2020-01-15 03:48:31
发布于 2020-01-15 03:48:31
89100
代码可运行
举报
文章被收录于专栏:原创分享原创分享
运行总次数:0
代码可运行

libuv初始化的时候会初始化信号处理相关的逻辑。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 保证只执行uv__signal_global_init一次
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_global_fini();
  // 新建一个管道用于互斥控制
  if (uv__make_pipe(uv__signal_lock_pipefd, 0))
    abort();
  // 先往管道写入数据,即解锁。后续才能顺利lock,unlock配对使用
  if (uv__signal_unlock())
    abort();
}

UV_DESTRUCTOR(static void uv__signal_global_fini(void)) {
  if (uv__signal_lock_pipefd[0] != -1) {
    uv__close(uv__signal_lock_pipefd[0]);
    uv__signal_lock_pipefd[0] = -1;
  }

  if (uv__signal_lock_pipefd[1] != -1) {
    uv__close(uv__signal_lock_pipefd[1]);
    uv__signal_lock_pipefd[1] = -1;
  }
}

经过一系列的操作后,主要是申请了一个用于互斥控制的管道,然后往管道里写数据。后面就可以使用lock和unlock进行加锁解锁。接着在第一个注册信号的时候,还会做一些初始化的工作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle) {
  int err;
  // 申请和libuv的通信管道并且注册io观察者
  err = uv__signal_loop_once_init(loop);
  if (err)
    return err;

  uv__handle_init(loop, (uv_handle_t*) handle, UV_SIGNAL);
  handle->signum = 0;
  handle->caught_signals = 0;
  handle->dispatched_signals = 0;

  return 0;
}

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__F_NONBLOCK);
  if (err)
    return err;
  // 设置信号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__signal_event
  uv__io_start(loop, &loop->signal_io_watcher, POLLIN);

  return 0;
}

上面的代码主要的工作有两个

1 申请一个管道,用于其他进程(libuv进程或fork出来的进程)和libuv进程通信。然后往libuv的io观察者队列注册一个观察者,libuv在poll io阶段会把观察者加到epoll中。io观察者里保存了管道读端的文件描述符loop->signal_pipefd[0]和回调函数uv__signal_event。

2 初始化信号相关的handle

接着会调uv__signal_start函数注册信号和处理函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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));
  if (signum == 0)
    return UV_EINVAL;
  // 注册过了,重新设置处理函数就行
  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
  first_handle = uv__signal_first_handle(signum);
  // 之前没有注册过该信号的处理函数或者oneshot规则比之前的严格则重新修改该信号的处理规则
  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 给进程注册一个信号和信号处理函数。主要是调用操作系统的函数来处理的,代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 给当前进程注册信号处理函数,会覆盖之前设置的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;
}

2 进程注册的信号和回调是在一棵红黑树管理的,每次注册的时候会往红黑树插入一个节点会修改他的节点。

至此,信号的注册就完成了。我们发现,在uv__signal_register_handler函数中有这样一句代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  sa.sa_handler = uv__signal_handler;

我们发现,不管注册什么信号,他的处理函数都是这个。我们自己的业务回调函数,是保存在handle里的。那么当有信号到来的时候。uv__signal_handler就会被调用。下面我们看看uv__signal_handler函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 信号处理函数,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;
  }

  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);

    assert(r == sizeof msg ||
           (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)));
    // 该handle收到信号的次数
    if (r != -1)
      handle->caught_signals++;
  }

  uv__signal_unlock();
  errno = saved_errno;
}

该函数遍历红黑树,找到注册了该信号的handle,然后封装一个msg写入管道(即可libuv通信的管道)。信号的处理就完成了。接下来在libuv的poll io阶段才做真正的处理。回到文章开头我们知道在poll io阶段。epoll会检测到管道loop->signal_pipefd[0]可读,然后会执行uv__signal_event函数。我们看看这个函数的代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 如果收到信号,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;

    /* `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;
      // 收到的信号和handle感兴趣的信号一致,执行回调
      if (msg->signum == handle->signum) {
        assert(!(handle->flags & UV_HANDLE_CLOSING));
        handle->signal_cb(handle, handle->signum);
      }
      // 处理信号个数
      handle->dispatched_signals++;
      // 只执行一次,恢复系统默认的处理函数
      if (handle->flags & UV_SIGNAL_ONE_SHOT)
        uv__signal_stop(handle);

      // 处理完了关闭
      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);
}

分支逻辑很多,我们只需要关注主要的。该函数从管道独处刚才写入的一个个msg。从msg中取出handle,然后执行里面保存的回调函数(即我们设置的回调函数)。至此。整个信号注册和处理的流程就完成了。整个流程总结如下:

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-01-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
libuv源码学习笔记:tcp-echo-server
事件循环是 libuv 功能的核心部分,它负责对 I/O 进行轮询,并基于不同的事件源执行它们的回调函数。libuv 的设计目标之一是为了让异步 I/O 操作变得简单易用,同时保持高性能。
晨星成焰
2024/10/20
1920
libuv源码学习笔记:tcp-echo-server
libuv源码阅读(21)--uvtee
uv_write_s 类型是由普通ref以及cb和一些写操作有关的信息组成,然后它需要一个 uv_stream_t* handle 来配合使用。
wanyicheng
2021/03/13
1.1K0
libuv源码阅读(22)--spawn
总结:父进程fork出子进程去执行指定的文件或者应用,双方根据参数设置的共享fd来通信,这个示例比较简单,父进程只需要等待结束捕获信号就可以了。
wanyicheng
2021/03/14
2.6K0
【Nodejs源码剖析】基于inotify的文件监听机制
Node.js中实现了基于轮询的文件监听机制,基于轮询的监听其实效率是很低的,因为需要我们不断去轮询文件的元数据,如果文件大部分时间里都没有变化,那就会白白浪费CPU。如果文件改变了会主动通知我们那就好了,这就是基于inotify机制的文件监听。Node.js提供的接口是watch。watch的实现和watchFile的比较类似。
theanarkh
2021/05/28
1.2K0
【Nodejs源码剖析】基于inotify的文件监听机制
libuv源码分析之unix域
unix域是一种基于单主机的进程间通信方式。实现模式类似tcp通信。今天先分析他的实现,后续会分析他的使用。在libuv中,unix域用uv_pipe_t表示。
theanarkh
2020/05/24
8920
libuv源码分析之stream第二篇
关闭流的写端就是相当于给流发送一个关闭请求,把请求挂载到流中,然后注册等待可写事件,在可写事件触发的时候就会执行关闭操作。这个我们后面分析。
theanarkh
2020/05/07
6920
libuv源码阅读(16)--signal
总结:信号处理handler是被插入到红黑树中,按照一定规则排序插入的,信号越小,不带oneshot等规则。信号处理函数统一触发信号管道可读,然后loop从信号io管道可读端读取信号结构体,执行这个信号上的handler的回调。大概主体流程就是这样的。跟我们平常自己写某些信号的handler的方法类似:注册信号和信号函数,触发信号管道可读,主循环捕获io可读事件,根据信号值调用对应回调。
wanyicheng
2021/03/13
2.2K1
libuv源码阅读(12)--change
可以看到 fs_event_s 也是由基础的handler和一个path 以及 它独有的字段组成
wanyicheng
2021/03/12
6840
nodejs源码解析之udp服务器
我们看到创建一个udp服务器很简单,首先申请一个socket对象,在nodejs中和操作系统中一样,socket是对网络通信的一个抽象,我们可以把他理解成对传输层的抽象,他可以代表tcp也可以代表udp。我们看一下createSocket做了什么。
theanarkh
2020/09/11
1.5K0
nodejs的http.createServer过程解析
发现_http_server.js也没有太多逻辑,继续看lib/net.js下的代码。
theanarkh
2019/03/15
1.7K0
nodejs的http.createServer过程解析
libuv源码阅读(15)--ref-timer
主要包含 timercb 和用于最小时间堆节点字段heap_node等;它由一个基础hanlder类型和自身独有的属性构成
wanyicheng
2021/03/13
6340
libuv源码阅读(6)--helloworld
每一种都是一种hanlder类型或者request类型,代表某种资源类型或者请求操作的包装结构体,里面的属性字段是为了支持它可以正常工作的而设置的:
wanyicheng
2021/03/12
7931
libuv之线程池以及线程间通信源码解析
libuv实现了一个线程池,该线程池在用户提交了第一个任务的时候初始化,而不是系统启动的时候就初始化。入口代码如下。
theanarkh
2019/03/15
1.7K0
libuv线程池和主线程通信原理
代码很简单,就是设置一下async_io_watcher的fd和回调,在epoll_wait返回的时候用到。再看uv__io_start。
theanarkh
2020/01/15
1.5K0
libuv之inotify源码分析
inotify是linux系统提供用于监听文件系统的机制。inotify机制的逻辑大致是 1 init_inotify创建一个inotify机制的实例,返回一个文件描述符。类似epoll。 2 inotify_add_watch往inotify实例注册一个需监听的文件(inotify_rm_watch是移除)。 3 read((inotify实例对应的文件描述符, &buf, sizeof(buf))),如果没有事件触发,则阻塞(除非设置了非阻塞)。否则返回待读取的数据长度。buf就是保存了触发事件的信息。 libuv在inotify机制的基础上做了一层封装。 今天分析一下libuv中的实现。我们从一个使用例子开始。
theanarkh
2020/05/08
1.1K0
libuv之inotify源码分析
libuv源码阅读(18)--progress
总结:用户自己初始化的async handler 也可以被插入到异步handler队列中,当管道[0]可读的时候,代表某个异步handler可以处理了,这时候遍历队列,处理pengding状态的handler。
wanyicheng
2021/03/13
5370
libuv小册之线程池篇
前言:最近开始写小册子,一篇篇来,写完了再整理总结到一起。循序渐进,重点是优先分析libuv的原理。其他的有时间再写,也希望大家一起。
theanarkh
2020/05/07
1.5K0
libuv之async.c源码解析
libuv的async.c实现了线程和主线程的通信。在uv_loop_init函数中对async进行初始化。
theanarkh
2019/03/15
1.2K0
libuv之async.c源码解析
libuv源码分析之stream第一篇
流的实现在libuv里占了很大篇幅,今天分析一下流的实现。首先看数据结构。流在libuv里用uv_stream_s表示,他属于handle族。继承于uv_handle_s。
theanarkh
2020/05/07
9270
「Nodejs进阶」一文吃透异步I/O和事件循环
本文讲详细讲解 nodejs 中两个比较难以理解的部分异步I/O和事件循环,对 nodejs 核心知识点,做梳理和补充。
用户6835371
2021/09/06
2.2K0
相关推荐
libuv源码学习笔记:tcp-echo-server
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验