为了账号安全,请及时绑定邮箱和手机立即绑定

nodejs事件循环阶段之poll io

标签:
Node.js

poll io是nodejs非常重要的一个阶段,文件io、网络io、信号处理等都在这个阶段处理。这也是最复杂的一个阶段。处理逻辑在uv__io_poll这个函数。这个函数比较复杂,我们分开分析。
    开始说poll io之前,先了解一下他相关的一些数据结构。
1 io观察者uv__io_t。这个结构体是poll io阶段核心结构体。他主要是保存了io相关的文件描述符、回调、感兴趣的事件等信息。
2 watcher_queue观察者队列。所有需要libuv处理的io观察者都挂载在这个队列里。libuv会逐个处理。
我们看如何初始化一个io观察者

// 初始化io观察者
void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd) {
  // 初始化队列,回调,需要监听的fd
  QUEUE_INIT(&w->pending_queue);
  QUEUE_INIT(&w->watcher_queue);
  w->cb = cb;
  w->fd = fd;
  // 上次加入epoll时感兴趣的事件,在执行完epoll操作函数后设置
  w->events = 0;
  // 当前感兴趣的事件,在再次执行epoll函数之前设置
  w->pevents = 0;
}

我们再看一下如何注册一个io观察到libuv。

void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
  // 设置当前感兴趣的事件
  w->pevents |= events;
  // 可能需要扩容
  maybe_resize(loop, w->fd + 1);

  if (w->events == w->pevents)
    return;
  // io观察者没有挂载在其他地方则插入libuv的io观察者队列
  if (QUEUE_EMPTY(&w->watcher_queue))
    QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);
  // 保存映射关系
  if (loop->watchers[w->fd] == NULL) {
    loop->watchers[w->fd] = w;
    loop->nfds++;
  }
}

uv__io_start函数就是把一个io观察者插入到libuv的观察者队列中,并且在watchers数组中保存一个映射关系。libuv在poll io阶段会处理io观察者队列。

https://img1.sycdn.imooc.com//5e9db7630001880505480342.jpg在这里插入图片描述
    下面我们开始分析poll io阶段。先看第一段逻辑。


 // 没有io观察者,则直接返回
 if (loop->nfds == 0) {
    assert(QUEUE_EMPTY(&loop->watcher_queue));
    return;
  }
  // 遍历io观察者队列
  while (!QUEUE_EMPTY(&loop->watcher_queue)) {
      // 取出当前头节点
    q = QUEUE_HEAD(&loop->watcher_queue);
    // 脱离队列
    QUEUE_REMOVE(q);
    // 初始化(重置)节点的前后指针
    QUEUE_INIT(q);
    // 通过结构体成功获取结构体首地址
    w = QUEUE_DATA(q, uv__io_t, watcher_queue);
    // 设置当前感兴趣的事件
    e.events = w->pevents;
    // 这里使用了fd字段,事件触发后再通过fd从watchs字段里找到对应的io观察者,没有使用ptr指向io观察者的方案
    e.data.fd = w->fd;
    // w->events初始化的时候为0,则新增,否则修改
    if (w->events == 0)
      op = EPOLL_CTL_ADD;
    else
      op = EPOLL_CTL_MOD;
    // 修改epoll的数据
    epoll_ctl(loop->backend_fd, op, w->fd, &e)
    // 记录当前加到epoll时的状态 
    w->events = w->pevents;
  }

第一步首先遍历io观察者,修改epoll的数据,即感兴趣的事件。然后准备进入等待,如果设置了UV_LOOP_BLOCK_SIGPROF的话。libuv会做一个优化。如果调setitimer(ITIMER_PROF,…)设置了定时触发SIGPROF信号,则到期后,并且每隔一段时间后会触发SIGPROF信号,这里如果设置了UV_LOOP_BLOCK_SIGPROF救护屏蔽这个信号。否则会提前唤醒epoll_wait。

  psigset = NULL;
  if (loop->flags & UV_LOOP_BLOCK_SIGPROF) {
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGPROF);
    psigset = &sigset;
  }
    /*
      http://man7.org/linux/man-pages/man2/epoll_wait.2.html
      pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
      ready = epoll_wait(epfd, &events, maxevents, timeout);
      pthread_sigmask(SIG_SETMASK, &origmask, NULL);
      即屏蔽SIGPROF信号,避免SIGPROF信号唤醒epoll_wait,但是却没有就绪的事件
    */
    nfds = epoll_pwait(loop->backend_fd,
                       events,
                       ARRAY_SIZE(events),
                       timeout,
                       psigset);
    // epoll可能阻塞,这里需要更新事件循环的时间
    uv__update_time(loop)        

在epoll_wait可能会引起主线程阻塞,具体要根据libuv当前的情况。所以wait返回后需要更新当前的时间,否则在使用的时候时间差会比较大。因为libuv会在每轮时间循环开始的时候缓存当前时间这个值。其他地方直接使用,而不是每次都去获取。下面我们接着看epoll返回后的处理(假设有事件触发)。

    // 保存epoll_wait返回的一些数据,maybe_resize申请空间的时候+2了
    loop->watchers[loop->nwatchers] = (void*) events;
    loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
    for (i = 0; i < nfds; i++) {
      // 触发的事件和文件描述符
      pe = events + i;
      fd = pe->data.fd;
      // 根据fd获取io观察者,见上面的图
      w = loop->watchers[fd];
      // 会其他回调里被删除了,则从epoll中删除
      if (w == NULL) {
        epoll_ctl(loop->backend_fd, EPOLL_CTL_DEL, fd, pe);
        continue;
      }
      if (pe->events != 0) {
        // 用于信号处理的io观察者感兴趣的事件触发了,即有信号发生。
        if (w == &loop->signal_io_watcher)
          have_signals = 1;
        else
          // 一般的io观察者指向回调
          w->cb(loop, w, pe->events);
        nevents++;
      }
    }
    // 有信号发生,触发回调
    if (have_signals != 0)
      loop->signal_io_watcher.cb(loop, &loop->signal_io_watcher, POLLIN);

这里开始处理io事件,执行io观察者里保存的回调。但是有一个特殊的地方就是信号处理的io观察者需要单独判断。他是一个全局的io观察者,和一般动态申请和销毁的io观察者不一样,他是存在于libuv运行的整个生命周期。async io也是。这就是poll io的整个过程。最后看一下epoll_wait阻塞时间的计算规则。

// 计算epoll使用的timeout
int uv_backend_timeout(const uv_loop_t* loop) {
  // 下面几种情况下返回0,即不阻塞在epoll_wait 
  if (loop->stop_flag != 0)
    return 0;
  // 没有东西需要处理,则不需要阻塞poll io阶段
  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;
  // idle阶段有任务,不阻塞,尽快返回直接idle任务
  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;
  // 同上
  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;
  // 同上
  if (loop->closing_handles)
    return 0;
  // 返回下一个最早过期的时间,即最早超时的节点
  return uv__next_timeout(loop);
}


欢迎关注 编程杂技 分享技术 交流技术

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
1
获赞与收藏
3

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消