Nodejs源码的阅读-事件循环的建立

来源:互联网 发布:知乎 南京装修工作室 编辑:程序博客网 时间:2024/05/16 06:48

Nodejs源码的阅读-事件循环的建立

 

解读基于node V0.2.0

 

我们知道nodejsc++这一层面主要的工作是建立事件循环,随后加载命令行的js文件交给V8执行,同时启动循环。所有异步操作都会扔到事件循环中,一旦事件队列空了,程序就会退出。

建立事件要从main函数开始看。

 

Main函数

这个函数完成4个动作

/***********

1.解析参数

2.设置libevlibeio的各种回调函数

3.初始化v8环境

4.开始加载js

***********/

int main(int argc, char *argv[]) 

 

1.解析参数是下面的代码:

 node::ParseArgs(&argc, argv);

这个函数会解析以减号(-)开头的参数,比如-help-version。直到遇到第一个不是以减号开始的参数,并记录这个位置。用于后面分离出交给js文件和其参数。

 

2.设置libevlibeio的各种回调函数就是建立循环的主要内容。

libev 是系统异步模型的简单封装,基本上来说,它解决了 epoll kqueuq 与 select之间 API 不同的问题。保证使用 livev 的 API 编写出的程序可以在大多数 *nix 平台上运行。不过它不是运行在主线程上的,所以不阻塞主线程,而是一旦有事件准备就绪,就执行相应回调进行报告。

由于非阻塞属性不能用于文件读写上面,所以为了解决异步读写文件,引入了libeio

Libeio是实现读写文件的异步接口,它内部包含请求队列和回执队列,一旦回执队列在空和非空之间转换时就会触发回调函数通知外部。

Libevlibeio共同实现了node的异步队列。其创建过程如下。

 

//ev_default_loop就是把一个全局的static loop赋给ev_default_loop_ptr,然后返回。这样就可以全局用define的宏调来调去,EVFLAG_AUTO是指自动选择循环方式(selectpoll等)。

ev_default_loop(EVFLAG_AUTO);

 

//ev_prepare是设置每次主循环之前要做的事情

//初始化一个watcher,每次循环前调用一次node::Tick

ev_prepare_init(&node::next_tick_watcher, node::Tick);

//启动watcher,也就是把watcher插入队列

ev_prepare_start(EV_DEFAULT_UC_ &node::next_tick_watcher);

//ev_prepare_start会给活跃数加1,这个活跃watcher的存在会使得循环队列永远不为空,永远无法退出循环。所以把活跃数减1,使得这个watcher不能用于维持队列的存在。

ev_unref(EV_DEFAULT_UC);

 

 

下面又用这个方式initstart了其他几个watcher

//每次主循环之后要做的

  ev_check_init(&node::gc_check, node::Check);

  ev_check_start(EV_DEFAULT_UC_ &node::gc_check);

  ev_unref(EV_DEFAULT_UC);

 

//ev_asyncwatcher上调用了ev_async_send将执行对应的回调,这两个与libeio的就绪列队相关,WantPollNotifier最终将在libeio就绪队列由空变非空时被调用。

  ev_async_init(&node::eio_want_poll_notifier, node::WantPollNotifier);

  ev_async_start(EV_DEFAULT_UC_ &node::eio_want_poll_notifier);

  ev_unref(EV_DEFAULT_UC);

//DonePollNotifier最终将在libeio就绪队列由非空变为空时触发

  ev_async_init(&node::eio_done_poll_notifier, node::DonePollNotifier);

  ev_async_start(EV_DEFAULT_UC_ &node::eio_done_poll_notifier);

  ev_unref(EV_DEFAULT_UC);

 

另外还初始化了其它几个watcher,但是没有立即start,后面补充小节会详细讲这个。

//ev_idle是在没有非ev_idle类型的watcher时才会执行

//ev_timer类型是周期性地执行

  ev_idle_init(&node::tick_spinner, node::Spin);

  ev_idle_init(&node::gc_idle, node::Idle);

  ev_timer_init(&node::gc_timer, node::CheckStatus, 5., 5.);

  ev_idle_init(&node::eio_poller, node::DoPoll);

 

接下来是初始化libeio

//EIOWantPoll(第一个参数)在异步请求就绪队列有空转为非空时被触发

//EIODonePoll(第二个参数)在异步请求就绪队列有非空转为空时被触发

//这两个触发的函数相应会唤醒之前创建的ev_async类型的两个watcher

eio_init(node::EIOWantPoll, node::EIODonePoll);

 

//设置一次poll执行几个回调,没执行完,poll就会返回-1,我们可以继续poll

//不一次性释放完,是为了防止一轮循环耗时太长,那些setTimeoutnextTrick的函数就得不到执行或者执行的时间就会不对,本该执行的函数会因此延迟到某些代码之后执行。

// Don't handle more than 10 reqs on each eio_poll(). This is to avoid race conditions. See test/simple/test-eio-race.js这是代码的注释,意思就是限制一次eio_poll()最大的回调个数,是为了防止竞态条件,即多个操作同时执行,执行的顺序不定,会导致结果不定。

eio_set_max_poll_reqs(10);

 

3.初始化V8环境

//初始V8

V8::Initialize();

//HandleScope是用来装local类型handle的容器

HandleScope handle_scope;

//设置发生致命错误的回调

V8::SetFatalErrorHandler(node::OnFatalError);

//contentjs执行环境,创建一个执行环境

Persistent<v8::Context> context = v8::Context::New();

//切换到这个执行环境

v8::Context::Scope context_scope(context);

 

4.加载js

只有一句代码

  // Create all the objects, load modules, do everything.

  node::Load(argc, argv);

到这里main函数就没什么内容了,接下来代码就进入了load函数,并在里面阻塞起来。

一旦函数返回就代表程序要退出了。

 

Load函数

接下来我们到load函数里面看看main函数建立起来的循环是怎么启动的。

/******

1.创建process,并给process绑定各种函数和变量和常量

2.编译并执行node.js这个js文件,传入process

******/

static void Load(int argc, char *argv[]) 

 

1.创建process,并给process绑定各种函数和变量和常量

//创建process

Local<FunctionTemplate> process_template = FunctionTemplate::New();

node::EventEmitter::Initialize(process_template);

process = Persistent<Object>::New(process_template->GetFunction()->NewInstance());

//用于对title设置和赋值

process->SetAccessor(String::New("title"),

                     ProcessTitleGetter,

                     ProcessTitleSetter);

//下面设置一系列变量,常量,函数

process->Set(String::NewSymbol("global"), global);

process->Set(String::NewSymbol("version"), String::New(NODE_VERSION));

......

//把命令行变量设置进去

process->Set(String::NewSymbol("argv"), arguments);

//把环境变量设置进去

process->Set(String::NewSymbol("env"), env);

//把各种函数设置进去,NODE_SET_METHOD设置的变量值是函数

NODE_SET_METHOD(process, "loop", Loop);

......

//初始化IOWatcher模块,并设置到process里面(Initialize函数内部执行的:

target->Set(String::NewSymbol("IOWatcher"), constructor_template->GetFunction());)

IOWatcher::Initialize(process);

//初始化Timer模块,并设置到process里面

Timer::Initialize(process);

//设置常量到process里面

DefineConstants(process);

 

2.编译并执行node.js这个js文件,传入process

//编译node.js的源代码,所有js代码都早就已经以数组的形式编译进来了。

Local<Value> f_value = ExecuteString(String::New(native_node),

                                       String::New("node.js"));

//转成函数

Local<Function> f = Local<Function>::Cast(f_value);

//process组装到argv

Local<Value> args[1] = { Local<Value>::New(process) };

//调用并传给node.js

f->Call(global, 1, args);

 

这里Load的代码完了,代码将在f->Call(global, 1, args);阻塞起来,一旦退出,程序就会返回main()并退出。但是这里还是没看到main中创建的事件循环的启动。

 

Node.js

下面我们就进入node.js里面去看看事件循环在哪里启动的。

 

/*********

1.任务队列

2.模块系统

3.进程信号处理器

4.定时器实现

5.控制台实现

6.启动时间循环

*********/

(function (process) {......});

 

这里直接跳到第6部分。

if (process.argv[1]) {

  //...执行js代码  

  module.runMain();

} else {

  //没有指定js文件则进入REPL模式,

  //REPL (Read-Eval-Print-Loop), a JavaScript command line进入js命令行模式

  repl.start();

}

//js命令行模式,则继续启动事件循环。

process.loop();

 

这里的loop也就是在Load函数中设置的loop函数:

NODE_SET_METHOD(process, "loop", Loop);

Loop函数有一句关键代码:

 ev_loop(EV_DEFAULT_UC_ 0);

至此事件循环开启。

 

 

补充

关于前面几个创建但是未启动的watcher的补充:

//ev_idle是在没有非ev_idle类型的watcher时才会执行

//ev_timer类型是周期性地执行

  ev_idle_init(&node::tick_spinner, node::Spin);

  ev_idle_init(&node::gc_idle, node::Idle);

  ev_timer_init(&node::gc_timer, node::CheckStatus, 5., 5.);

  ev_idle_init(&node::eio_poller, node::DoPoll);

 

1.ev_idle_init(&node::tick_spinner, node::Spin);

这个是在你调用process.nextTick()的时候被start,目的是保证事件循环不为空,因为一旦空了,循环就会退出。为了新放进去的任务被执行到,循环不能退出,因此加入一个watcher。因此当Tick()执行之后,所有任务被完成,这个watcher任务就完成了,也就被stop了。

 

2.ev_idle_init(&node::gc_idle, node::Idle);

这个watcher的任务是告诉V8现在空闲,虽然其实可能并不空闲,只是内存占多了,V8你可以做些gc之类的事情了。

他不在任务循环中,而是在另一个循环中。因此不会随着任务循环的阻塞而阻塞。

对于gc_idle这个watcher的启动,先来看另一个watchergc_check

ev_check_init(&node::gc_check, node::Check);这个watcher在每次循环之后执行,会必然启动gc_timer,选择性启动gc_idle。而gc_timer也会选择性启动gc_idle.

而对于gc_idle这个watcher的停止,是在gc_idle自身,它在告知V8现在空闲之后,就自己停止自己。

 

3.ev_timer_init(&node::gc_timer, node::CheckStatus, 5., 5.);

这个watcher是定时检查内存情况,选择性启动gc_idle

它在每次循环之后被gc_check这个watcher启动。

然后在gc_idle停止它自身的同时,停止gc_timer

但是由于每次循环之后被gc_check这个watcher启动,所以基本上这个watcher一直在运行。

 

4.ev_idle_init(&node::eio_poller, node::DoPoll);

这个watcher的作用是回调libeio中的就绪队列。

在“Nodejs源码的阅读-事件循环的建立”中有看到这个调用,eio_set_max_poll_reqs(10);

这个调用是避免一次eio_poll执行过多,延误任务队列的执行时间。所以如果异步io队列的任务过多,多余10个,那么就要分次执行。

所以这个watcher的启动时机是在一次eio_poll没有取完全部任务时。

而这个watcher停止时机是在一次eio_poll取完了全部的时候。

 

 

这里再回顾看下这几个watcher

  ev_idle_init(&node::tick_spinner, node::Spin);

  ev_idle_init(&node::gc_idle, node::Idle);

  ev_timer_init(&node::gc_timer, node::CheckStatus, 5., 5.);

  ev_idle_init(&node::eio_poller, node::DoPoll);

 

要注意:

ev_idle是在没有非ev_idle类型的watcher时才会执行

ev_timer类型是周期性地执行

这几个中除了gc_idle是在另一个循环中,其他都在我们使用的default循环中。

 

 

 

0 0