Nodejs源码的阅读-事件循环的建立
解读基于node V0.2.0
我们知道nodejs在c++这一层面主要的工作是建立事件循环,随后加载命令行的js文件交给V8执行,同时启动循环。所有异步操作都会扔到事件循环中,一旦事件队列空了,程序就会退出。
建立事件要从main函数开始看。
Main函数
这个函数完成4个动作
/***********
1.解析参数
2.设置libev和libeio的各种回调函数
3.初始化v8环境
4.开始加载js
***********/
int main(int argc, char *argv[])
1.解析参数是下面的代码:
node::ParseArgs(&argc, argv);
这个函数会解析以减号(-)开头的参数,比如-help,-version。直到遇到第一个不是以减号开始的参数,并记录这个位置。用于后面分离出交给js文件和其参数。
2.设置libev和libeio的各种回调函数就是建立循环的主要内容。
libev 是系统异步模型的简单封装,基本上来说,它解决了 epoll ,kqueuq 与 select之间 API 不同的问题。保证使用 livev 的 API 编写出的程序可以在大多数 *nix 平台上运行。不过它不是运行在主线程上的,所以不阻塞主线程,而是一旦有事件准备就绪,就执行相应回调进行报告。
由于非阻塞属性不能用于文件读写上面,所以为了解决异步读写文件,引入了libeio。
Libeio是实现读写文件的异步接口,它内部包含请求队列和回执队列,一旦回执队列在空和非空之间转换时就会触发回调函数通知外部。
Libev和libeio共同实现了node的异步队列。其创建过程如下。
//ev_default_loop就是把一个全局的static loop赋给ev_default_loop_ptr,然后返回。这样就可以全局用define的宏调来调去,EVFLAG_AUTO是指自动选择循环方式(select,poll等)。
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);
下面又用这个方式init并start了其他几个watcher。
//每次主循环之后要做的
ev_check_init(&node::gc_check, node::Check);
ev_check_start(EV_DEFAULT_UC_ &node::gc_check);
ev_unref(EV_DEFAULT_UC);
//在ev_async的watcher上调用了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。
//不一次性释放完,是为了防止一轮循环耗时太长,那些setTimeout和nextTrick的函数就得不到执行或者执行的时间就会不对,本该执行的函数会因此延迟到某些代码之后执行。
// 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);
//content是js执行环境,创建一个执行环境
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的启动,先来看另一个watcher:gc_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循环中。