nodejs代码初探之nodejs启动
阅读原文时间:2023年07月08日阅读:3

nodejs启动

入口在node_main.cc,解析参数后进入node.cc 中的node::Start()

V8::Initialize() //初始化v8
SetupProcessObject() //在v8中创建process对象
Load() //bootstrap,加载执行node.js
uv_run() //调用libuv,开始异步事件polling和处理
模块装载

为了和用户编写的模块做区别,我将nodejs自带的模块称为系统模块,其中C++模块在src目录中,node_开头的那些文件大部分是,javascript模块在lib目录中。模块装载是指将C++或者javascript模块加载到v8引擎中,生成对象系统,可供javascript代码调用。nodejs采用了延迟加载的策略,系统模块只有用到的时候才会加载,加载完后放到binding_cache中。

上面提到,nodejs在启动的时候会创建process对象。process.binding()方法用来将系统模块加载到v8中去(参见node.cc中的Binding函数)。

C++模块的装载比较直接,一个典型的C++模块具有如下形式,在register_func中将对象,函数等注册到v8中,这样javascript代码就能调用它们了。

void register_func(Handle target) {
// 模块注册函数
}
NODE_MODULE(node_test, register_func) // 加入C++模块列表
对于上面的模块,调用process.binding(“test”)就可以装载。

对于javascript模块,装载稍微麻烦点。首先,lib目录中的js文件,编译nodejs的时候会通过js2c.py将它们转换成C数组,放在中间文件node_natives.h中,这样子,这些js文件就已经成为代码的一部分,nodejs就不需要再读取这些系统js文件了,加快了装载速度。包括之前提到的用来引导系统的node.js也是通过这种方式处理的,你可以通过require(‘native_module’)来使用node.js。

好了,现在我们知道了系统js文件已经作为C数组编译到代码中了,怎么将它们装入v8?分两步来进行,第一步,引导脚本node.js调用我们之前提到的process.binding(‘natives’),将它们作为数组装入v8(参见node.cc中的Binding函数和node_javascript.cc中的DefineJavaScript函数)。但是这时候装载还未完成,因为还没有执行这些js文件。第二步,调用node.js中的NativeModule.require()装载单个js模块,在执行之前,系统会给js模块加个wrapper,传入几个常用对象,这样模块中就能使用系统传入的这几个对象了。

NativeModule.wrapper =
['(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
上面介绍的模块都是静态的,其实nodejs也可以装载动态模块,unix下的共享库,Windows下的DLL。前面提到的process对象有一个dlopen方法,可以用来装载动态模块。对动态模块有一个要求,就是要导出一个init函数,系统会调用此函数来将对象装载到v8中。

void init(Handle target) {
// 模块注册函数
}
最后,还有一个问题,对于用户编写的模块,系统是如何加载的?nodejs通过模块module.js来管理和装载用户模块,装载使用Module.prototype.require(),包括node_modules的路径处理等等细节参见module.js文件。

说完了模块加载,扩展nodejs是很容易的事情了。大部分情况我们是不需要扩展nodejs的。在一些特殊的场合下,可能要求对nodejs做扩展,比如有大量legacy的C/C++的代码想要集成到nodejs。

异步调用的实现

接下来说说nodejs中的异步调用是如何实现的。这块也是个人兴趣较大的地方,在之前的工作中也实现过异步的操作模式,无非是线程消息队列嘛,但当时我的实现不是通用的模块。

前面提到函数node::Start()在装载完所有模块后,就开始执行消息队列的pooling和处理,调用uv_run(uv_default_loop())。uv_default_loop()函数会初始化uv,也会初始化并返回一个default loop。参见core.c,既然我们已经进入libuv的领地了,先简单介绍一下libuv。libuv显然是要抹平操作系统的差异,封装libev,libeio和Windows的io completion port,向用户提供一个跨平台的异步操作库。

libuv 简介

libuv 的API是C风格的,很容易读。你可能觉得uv.h中暴露了太多的数据结构了,不够简洁,我想是因为libuv涵盖的内容非常的广泛,从网络,pipe,文件,终端等等,包罗万象。而且uv.h也不是接口的全部,还有两个头文件,uv-unix.h和uv-win.h,里面定义了操作系统specific的数据结构。其实,libuv已经对接口的简化做了一些努力,比如说,通过uv_write一个函数,我们可以写TCP,Pipe和tty。

因为libuv涵盖广泛,我们的目的只是为了了解它如何与nodejs,v8协调工作的,不会面面俱到。有几个重要的数据结构有必要先了解一下:

#define UV_HANDLE_FIELDS \
uv_loop_t* loop;
uv_handle_type type;
uv_close_cb close_cb;
void* data;
UV_HANDLE_PRIVATE_FIELDS

/* The abstract base class of all handles. */
struct uv_handle_s {
UV_HANDLE_FIELDS
};
uv_handle_s是其它handle的父类,比如说,uv_tcp_s就是它的子类,在那里你能找到socket。loop字段表明它属于哪个loop,handle里还有一些callback函数,异步调用通常会有两个参数,一个是handle,一个是callback函数,调用完成的时候,libuv会调用callback函数。data字段是留给使用者的,nodejs实现异步机制的时候会用到。

#define UV_REQ_FIELDS \
uv_req_type type;
void* data;
UV_REQ_PRIVATE_FIELDS

/* Abstract base class of all requests. */
struct uv_req_s {
UV_REQ_FIELDS
};
这是所有request的父类。loop维护一个request queue。data字段有时用来存放handle。
再来看loop,这是libuv里面最关键的数据结构了,一般会指定一个线程负责一个loop的处理,nodejs只使用了一个loop,由主线程负责对它进行处理。

struct uv_loop_s {
UV_LOOP_PRIVATE_FIELDS
uv_ares_task_t* uv_ares_handles_;
uv_async_t uv_eio_want_poll_notifier;
uv_async_t uv_eio_done_poll_notifier;
uv_idle_t uv_eio_poller;
uv_counters_t counters;
uv_err_t last_err;
void* data;
};
我们只看Windows平台(参看uv-win.h)

#define UV_LOOP_PRIVATE_FIELDS \
HANDLE iocp;
int refs;
int64_t time;
uv_req_t* pending_reqs_tail;
uv_handle_t* endgame_handles;
……
我们已经知道有一些handles会和loop关联在一起,字段refs就是和它相关的handle的数量。它还会有一个请求队列,pending_reqs_tail。对windows来说,它有一个io completion port。初始化loop的时候会创建一个completion port,之后其它handles可以加入,通过这个port来监控事件。比如,当有新的socket建立的时候,再调用一次CreateIOCompletionPort()可以使用这个port来监控socket事件。
了解了数据结构以后,程序怎么运行应该就大致有数了。来看一下执行过程。

#define UV_LOOP_ONCE(loop, poll) \
do {
uv_update_time((loop));
uv_process_timers((loop));

/* Call idle callbacks if nothing to do. */
if ((loop)->pending_reqs_tail == NULL &&
(loop)->endgame_handles == NULL) {
uv_idle_invoke((loop));
}

uv_process_reqs((loop));
uv_process_endgames((loop));

if ((loop)->refs <= 0) {
break;
}

uv_prepare_invoke((loop));

poll((loop), (loop)->idle_handles == NULL &&
(loop)->pending_reqs_tail == NULL &&
(loop)->endgame_handles == NULL &&
(loop)->refs > 0);

uv_check_invoke((loop));
} while (0);

#define UV_LOOP(loop, poll)
while ((loop)->refs > 0) {
UV_LOOP_ONCE(loop, poll)
}
函数poll()检查completion port,有事件发生的时候,产生一条request,插入到请求队列中。
函数uv_process_reqs()从消息队列中取出请求,处理请求。此函数在处理请求的过程中可能会调用用户传入的callback。
函数uv_process_endgames()清理已经关闭的handle,同时减掉refs。
对libuv的介绍到此打住,不再详细展开了。

nodejs异步调用的实现

接下来分析一下nodejs是如何将libuv和v8拼接在一起从而实现javascript代码中的异步函数调用的。
其实看一个类就一目了然了,它就是HandleWrap,参见文件handle_wrap.h/handle_wrap.cc

HandleWrap::HandleWrap(Handle object, uv_handle_t* h) {
unref = false;
handle__ = h;
if (h) {
h->data = this;
}

HandleScope scope;
assert(object_.IsEmpty());
assert(object->InternalFieldCount() > 0);
object_ = v8::Persistent::New(object);
object_->SetPointerInInternalField(0, this);
}
handle_ //通过它调用libuv。
object_ //v8中的javascript对象。
this //Wrap自己,C++对象
从构造函数可以看出,HandleWrap的作用就像是一座桥,把this传给了handle_,同时也把this传给了object_。这个C++对象将v8中的javascript对象和libuv中的C对象(handle)联通了。C/C++/javascript,完美的组合,是不是很美妙?

HandleWrap是所有Wrap类的父类。为了看得更清楚,我们举一个具体的例子。TCPWrap,参见tcp_wrap.cc

// 这个函数会被注册到javascript世界,成为TCP()对象的一个方法,javascript代码调用TCP().listen()会来到这里。
Handle TCPWrap::Listen(const Arguments& args) {
HandleScope scope;

UNWRAP

int backlog = args[0]->Int32Value();

// 使用handle_和OnConnection 函数调用libuv,connection事件的时候,libuv会回调OnConnection。
int r = uv_listen((uv_stream_t*)&wrap->handle_, backlog, OnConnection);

// Error starting the tcp.
if (r) SetErrno(uv_last_error(uv_default_loop()));

return scope.Close(Integer::New(r));
}

// OnConnection函数在这里
void TCPWrap::OnConnection(uv_stream_t* handle, int status) {
HandleScope scope;
// 下面两行不用再解释了吧。从handle拿到C++对象this。
TCPWrap* wrap = static_cast(handle->data);
assert(&wrap->handle_ == (uv_tcp_t*)handle);

// We should not be getting this callback if someone as already called
// uv_close() on the handle.
assert(wrap->object_.IsEmpty() == false);

Handle argv[1];

if (status == 0) {
// 建一个新的javascript TCP()对象,并用它去Accept一个新的连接。
// Instantiate the client javascript object and handle.
Local client_obj = Instantiate();

// Unwrap the client javascript object.
assert(client_obj->InternalFieldCount() > 0);
TCPWrap* client_wrap =
static_cast(client_obj->GetPointerFromInternalField(0));

// Accept新的连接。
if (uv_accept(handle, (uv_stream_t*)&client_wrap->handle_)) return;

// Successful accept. Call the onconnection callback in JavaScript land.
argv[0] = client_obj;
} else {
SetErrno(uv_last_error(uv_default_loop()));
argv[0] = v8::Null();
}

// 进入javascript世界,并将新建的TCP()对象作为一个参数传入。
MakeCallback(wrap->object_, "onconnection", 1, argv);
}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章