异步是Node的特性之一,下面在Windows下,简要分析Node异步I/O的实现

事件循环与观察者

  • 事件循环
    事件循环模型是Node回调机制的支撑,在进程启动时,Node便会创建一个循环,每一次循环称之为Tick,每个Tick的过程即为查看是否有事件等待处理,若有,就取出事件及其关联的回调函数,若函数存在,就予以执行,进入下一个循环,若不再有事件处理,就退出程序。
  • 观察者
    在每个Tick的过程中,要判断是否有事件要处理,就要用到观察者的概念,每个事件循环中可以有一个或多个观察者,判断是否有事件要处理,就是向这些观察者询问是否有要处理的事件。

该情景就像餐馆做菜,后厨每做完一盘菜,就询问前台是否还有新的菜要做,若没有,就可以休息下班,若有,就要继续做,在此过程中,后厨每进行一次询问,就是一次Tick,而前台就是观察者,客人向前台下单就是一个事件,客人要求做的菜就是事件要处理的数据,而菜做完后要如何处理(打包还是上桌,上到哪桌)就是该事件的回调。前台可以有多个,客人可以不止向一个前台点餐,每个前台可以接受多个客人点餐,客人每次点菜可以点多个,即每个事件循环中,可以有多个观察者,一个观察者里也可能有多个事件,每个事件可能触发多个回调。

Node中,I/O请求就是产生相应的事件,同样,有文件I/O观察者与之对应。

请求对象

Node中,异步I/O调用时,回调函数不用开发人员调用,从发起调用到内核执行完I/O操作的过程中,存在一种叫做请求对象的中间产物。以fs.open()调用为例,其源码调用实现如下:

fs.open = function(path, flags, mode, callback) {
   callback = makeCallback(arguments[arguments.length - 1]);
   mode = modeNum(mode, 438 /*=0666*/);

   if (!nullCheck(path, callback)) return;
   binding.open(pathModule._makeLong(path),    //调用binding.open()
               stringToFlags(flags),
               mode,
               callback);
}

可以看到,fs.open()通过调用binding.open()来进行操作,binding的定义如下:

var binding = process.binding('fs')

process.binding()可以看作一个桥梁,由于Node的一些核心模块是由C/C++编写的,process.binding()即可看作对该种模块的绑定,其接收一个参数,将该参数对应的C/C++模块取出绑定,这里接受了参数fs,证明该模块是由C/C++实现的,将该模块的实现取出绑定给了bindingfs对应的C/C++实现模块为node_file.cc,其为文件操作给出了最终实现,因此,最终调用到的函数为node_file.cc中的Open函数,其源码如下:

static void Open(const FunctionCallbackInfo<Value>& args) {
  // ...

  String::Utf8Value path(args[0]);
  int flags = args[1]->Int32Value();
  int mode = static_cast<int>(args[2]->Int32Value());

  if (args[3]->IsFunction()) {  //如果存在回调
    ASYNC_CALL(open, args[3], *path, flags, mode)
  } else {
    SYNC_CALL(open, *path, *path, flags, mode)
    args.GetReturnValue().Set(SYNC_RESULT);
  }
}

Open中,获取到传入的所有参数,并将其传送给SYNC_CALL,其源码如下:

#define ASYNC_CALL(func, callback, ...)                                       \
  Environment* env = Environment::GetCurrent(args.GetIsolate());              \  //获取当前系统环境
  FSReqWrap* req_wrap = new FSReqWrap(env, #func);                            \  //请求对象
  int err = uv_fs_ ## func(env->event_loop(),                                 \  //根据平台执行相应函数
                           &req_wrap->req_,                                   \ 
                           __VA_ARGS__,                                       \
                           After);                                            \
  req_wrap->object()->Set(env->oncomplete_string(), callback);                \  //将回调函数放置在oncomplete_string()属性上
  req_wrap->Dispatched();                                                     \  //线程分发
  if (err < 0) {                                                              \
    uv_fs_t* req = &req_wrap->req_;                                           \
    req->result = err;                                                        \
    req->path = NULL;                                                         \
    After(req);                                                               \
  }                                                                           \
  args.GetReturnValue().Set(req_wrap->persistent());

SYNC_CALL首先获取当前系统环境,若是 *nix平台,则执行dep/uv/src/unix/fs.c中的uv_fs_open方法,若是windows平台,则调用dep/uv/src/win/fs.c中的uv_fs_open方法,在其调用过程中,会创建一个FREeqWrap请求对象,JavaScript层所传入的参数和当前方法都会被封装在这个请求对象中,将回调函数放置在oncomplete_string()属性上,封装完毕后,将该对象分发入线程池等待运行。至此,JavaScript调用立即返回,并执行当前任务的后续操作,即达到了异步的目的。

执行回调

上面已经组装好了请求对象并送入了I/O线程池等待执行,下一步便是回调通知。当线程池的I/O操作调用完毕后,会将获取到的结果放到req->result属性上,并通知IOCP(I/O 完成端口,是windows下的内核级异步I/O实现),向IOCP提交执行状态,在整个过程中,每次Tick执行过程中,都会调用IOCP的相关方法检查线程池中是否有已经完成的请求,若有,就将该请求对象放入I/O观察者的队列中,将其当作事件处理。I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,oncomplete_string()属性作为方法,调用执行,达到调用JavaScript中传入的回调函数的目的。

至此,整个异步I/O的操作流程从发出请求到执行回调全部结束。