libuv源码粗读(5):uv__io_t io观察者句柄结构体和uv__io_poll介绍
发布于 1 个月前 作者 xtx1130 426 次浏览 来自 分享

由于uv__io_t和uv__io_poll的强相关性,所以在这里就一起介绍了。本篇文章会通过发起一个http request,通过断点调试,详细介绍uv__io_t句柄结构体和event-loop的poll阶段

uv__io_t声明

unix.h切入:

struct uv__io_s {
  uv__io_cb cb;
  void* pending_queue[2];
  void* watcher_queue[2];
  unsigned int pevents; /* Pending event mask i.e. mask at next tick. */
  unsigned int events;  /* Current event mask. */
  int fd;
  UV_IO_PRIVATE_PLATFORM_FIELDS
};
  • watcher_queue观察者队列
  • pevents 下一个事件的位掩码(bitmask)
  • events 当前事件位掩码(bitmask)

由于本人在MacOS环境下测试,所以 UV_IO_PRIVATE_PLATFORM_FIELDS宏展开为:

#define UV_IO_PRIVATE_PLATFORM_FIELDS                                         \
  int rcount;                                                                 \
  int wcount;                                                                \
  • rcount (read count)用于保存在POLLIN事件中可读取的字节量
  • wcount(write count)用于保存在POLLOUT事件中可写缓存区的字节量

这两个结构体成员变量取值全部由kevent->data返回。

uv__io_poll详解

变量声明

首先我们看一下uv__io_poll中变量的定义: image 前端发起了一个http request,而这个request的回调会在poll阶段被触发,上图便是刚进入到poll阶段时候的截图,我们介绍几个主要的变量:

  • events[1024] 为kevent结构体数组(在下面的介绍中我们会称之为eventlist),每一项均为一个kevent结构体变量,如图所示: image
  • ev 为kevent的指针,它会指向eventlist 中放置事件的kevent结构体变量
  • nevents 为改变的eventlist数量(截图的时候nevents为2,因为在断点调试的时候有一个未完成的favicon.ico请求,刷新页面之后的http request请求在老页面favicon.ico请求之后。)
  • revents 为要执行的事件的bit掩码,主要用来传递给cb
  • q 用于取出当前的watcher_queue(即保存当前w的地方)
  • wuv__io_t的指针,指向当前的io观察者
  • filter为 kqueue过滤器

io观察者

下面我们看一下主要的代码逻辑:

    q = QUEUE_HEAD(&loop->watcher_queue);
    QUEUE_REMOVE(q);
    QUEUE_INIT(q);
    w = QUEUE_DATA(q, uv__io_t, watcher_queue);

这段代码是通过QUEUE_DATA宏,从watcher_queue中取出当前的io观察者w。拿到了当前的io观察者,后面的事情就好办了。

事件(kqueue)初始化

事件的初始化是通过EV_SET宏实现的:

    if ((w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0) {
      filter = EVFILT_READ;
      fflags = 0;
      op = EV_ADD
      // ...
      EV_SET(events + nevents, w->fd, filter, op, fflags, 0, 0);
      if (++nevents == ARRAY_SIZE(events)) {
        // ...
      }
    }

首先,我们通过位运算来判断本次请求的事件,由于是http request,所以这里就进入到了POLLIN事件中,进入流程之后的第一件事情就是改写过滤器为EVFILT_READ,之后通过EV_SET宏来初始化kevent结构体。 EV_SET宏形参介绍:

EV_SET(_kev, ident, filter, flags, fflags, data, udata);
  • _kev kevent 结构体
  • ident fd文件描述符
  • filter kqueue过滤器
  • flags kqueue事件操作
  • fflags 特定过滤器标志
  • data 特定过滤器的数据
  • udata 用户传递的自定义数据

通过形参的介绍,估计大家对上面EV_SET(events + nevents, w->fd, filter, op, fflags, 0, 0);这段代码的理解就比较清晰了。EV_SET会定义kqueue结构体,并通过这些传参对结构体初始化。在这之后通过++nevents记录了改变的eventlist数量。

事件(kevent)注册

事件的注册是通过kevent实现的:

    nfds = kevent(loop->backend_fd,
                  events,
                  nevents,
                  events,
                  ARRAY_SIZE(events),
                  timeout == -1 ? NULL : &spec);

kevent的定义如下:

kevent(int kq, const struct kevent	*changelist, int nchanges,
	 struct kevent *eventlist, int nevents,
	 const struct timespec *timeout);
  • kq kqueue 描述符
  • changelist 增加或删除事件之后的eventlist 的指针
  • nchanges 增加或删除的数量
  • eventlist 为指向eventlist的指针
  • nevents eventlist的大小
  • timeout 超时时间

通过参数的介绍,大家可以清晰的理解上文中的kevent调用,而kevent的返回值为待处理的事件数,在这里由ndfs来记录。

事件监听

事件的监听则是通过ndfs取出eventlist中的kevent,然后通过对filter过滤器的判断,实现不同事件类型的监听:

for (i = 0; i < nfds; i++) {
      ev = events + i;
      fd = ev->ident;
      // ...
      if (ev->filter == EVFILT_READ) {
        if (w->pevents & POLLIN) {
          revents |= POLLIN;
          w->rcount = ev->data;
        } else {
          // ...
        }
      }
      // ....
      if (w == &loop->signal_io_watcher)
        have_signals = 1;
      else
        w->cb(loop, w, revents);
}

下面为单步的详解:

  • events + i拿到待处理kevent的指针
  • ev->filter == EVFILT_READ过滤器来对事件类型进行过滤(同级if语句还有别的类型判断,在这里不做过多阐述)
  • w->pevents & POLLIN实现对POLLIN事件的判断
  • w->rcount = ev->data;把数据保存到了w即io观察者中
  • 最终通过w->cb(loop, w, revents);触发回调

参考资料:

原文地址:https://github.com/xtx1130/blog/issues/34。如果文中介绍或者逻辑有问题,欢迎大佬留言斧正。

by 小菜

回到顶部