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观察者队列。
下面我们开始分析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);
}
欢迎关注 编程杂技 分享技术 交流技术