Node.js 是如何跑起来的( 三 )

Libuv 架构Libuv 是 NodeJS 的核心组件,是一个跨平台的处理异步 I/O 请求的 C 库,从架构来看,它把各类请求主要分为两大类:网络 I/O 相关请求,以及文件 I/O、DNS Ops 以及 User code 组成的请求 。
对于网络 I/O 相关请求,根据 OS 平台的不同,分别采用了 linux 的 epoll、OSX 和 BSD 类 OS 的 kqueue、SunOS 的 event ports 以及 windows 的 IOCP 等 I/O 读写机制 。
对于 File I/O 为代表的请求,则使用线程池实现异步请求处理,具有更好的跨平台特性 。
事件循环 event loop在 Node 应用启动后,就会进入 Libuv 事件循环中,每一轮循环 Libuv 都会处理维护在各个阶段的任务队列的回调节点,在回调节点中可能会产生新的任务,任务可能在当前循环或是下个循环继续被处理 。
以下是 Libuv 的执行流程图:
下面简述一下各个阶段代表的含义:

  1. 首先判断当前事件循环是否处于 alive 状态,否则退出整个事件循环 。alive 状态表示是否有 active 状态的 handle 和 request,closing 状态的 handle
  2. 基于系统时间更新时间戳
  3. 判断由定时器组成的小顶堆中那个节点超时,超时则执行定时器回调
  4. 执行 pending 回调任务,一般 I/O 回调添加的错误或写数据成功的任务都会在下一个事件循环的 pending 阶段执行
  5. 执行 idle 阶段的回调任务
  6. 执行 prepare 阶段的回调任务
  7. 调用各平台的 I/O 读写接口,最多等待 timeout 时间(定时器最快过期时间),期间如果有数据返回,则执行 I/O 对应的回调
  8. 执行 check 阶段的回调任务
  9. 执行 closing 阶段的回调任务
  10. 重新回到流程 1
?源码概览?// src/unix/core.cint uv_run(uv_loop_t* loop, uv_run_mode mode) {int timeout;int r;int can_sleep;r = uv__loop_alive(loop);if (!r)uv__update_time(loop);while (r != 0 && loop->stop_flag == 0) {uv__update_time(loop);uv__run_timers(loop);can_sleep =QUEUE_EMPTY(&loop->pending_queue) && QUEUE_EMPTY(&loop->idle_handles);uv__run_pending(loop);uv__run_idle(loop);uv__run_prepare(loop);timeout = 0;if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)timeout = uv__backend_timeout(loop);uv__metrics_inc_loop_count(loop);// 动态设置 epoll_wait 的超时时间uv__io_poll(loop, timeout);for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++)uv__run_pending(loop);uv__metrics_update_idle_time(loop);uv__run_check(loop);uv__run_closing_handles(loop);if (mode == UV_RUN_ONCE) {uv__update_time(loop);uv__run_timers(loop);}r = uv__loop_alive(loop);if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)break;}/* The if statement lets gcc compile it to a conditional store. Avoids* dirtying a cache line.*/if (loop->stop_flag != 0)loop->stop_flag = 0;return r;}任务调度了解了 Libuv 的事件循环流程,接下来结合 JS 代码具体看看 NodeJS 是如何进行任务调度的 。
image.png
目前,主要有五种主要类型的队列被 Libuv 的事件循环所处理:
  • 过期或是定期的时间队列,由 setTimeout 或 setInterval 函数所添加的任务
  • Pending 队列,主要存放读写成功或是错误的回调
  • I/O 事件队列,主要存放完成 I/O 事件时的回调
  • Immediates 队列,采用 setImmediate 函数添加的任务,对应 libuv 的 check 阶段
  • Close 任务队列,主要存放 close 事件回调
除了以上五种主要的任务列表,还有额外两种不属于 libuv 而是作为 NodeJS 一部分的任务队列:
  • Next Ticks 队列,采用 process.nextTick 添加的任务
  • 其他微任务队列,例如 promise callback 等
nextTicks 队列和其他微任务队列会在事件循环每一阶段穿插调用,nextTicks 优先级会比其他微任务队列更高 。
?示例?// timer -> pending -> idle -> prepare -> poll io -> check -> close// timer phasesetTimeout(() => {Promise.resolve().then(() => {console.log('promise resolve in timeout');process.nextTick(() => {console.log('tick task in timeout promise');});})process.nextTick(() => {console.log('tick task in timeout');process.nextTick(() => {console.log("tick task in timeout->tick");})});console.log('timer task');}, 0);// check phasesetImmediate(() => {process.nextTick(() => {console.log('imeediate->tick task')});console.log('immediate task');});Promise.resolve().then(() => {console.log('promise resolve');});process.nextTick(() => {console.log("tick task");});console.log('run main thread');


推荐阅读