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<Object> 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<Object> 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> 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<v8::Object>::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<Value> 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<TCPWrap*>(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<Value> argv[1];

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

// Unwrap the client javascript object.
assert(client_obj->InternalFieldCount() > 0);
TCPWrap* client_wrap =
static_cast<TCPWrap*>(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);
}

nodejs代码初探之nodejs启动的更多相关文章

  1. 3分钟干货学会使用node-inspector调试NodeJS代码

    使用node-inspector调试NodeJS代码 任何一门完备的语言技术栈都少不了健壮的调试工具,对于NodeJS平台同样如此,笔者研究了几种调试NodeJS代码的方式,通过对比,还是觉得node ...

  2. 将nodejs代码部署到阿里云服务器

    概述 最近在做一个小项目,其中用nodejs做了个数据转发的接口,之后需要将这部分代码部署到服务器上面,并使用Nginx做反向代理.期间使用搜索引擎大量查阅了其他同鞋的经验,不过写的大多很笼统,因此踩 ...

  3. 使用node-inspector调试NodeJS代码

    使用node-inspector调试NodeJS代码 任何一门完备的语言技术栈都少不了健壮的调试工具,对于NodeJS平台同样如此,笔者研究了几种调试NodeJS代码的方式,通过对比,还是觉得node ...

  4. 使用NodeJsScan扫描nodejs代码检查安全性

    使用NodeJsScan扫描nodejs代码检查安全性1.下载源码:https://github.com/ajinabraham/NodeJsScan2.下载Windows版docker toolbo ...

  5. 异步nodejs代码的同步样子写法样例

    异步nodejs代码的同步样子写法样例 js的异步嵌套太深代码将不好看.尤其在用node的时候这种情况会大量出现. 这里用node连接redis,做一个用户注册的简单例子来说明.例如用redis做存储 ...

  6. 使用nodejs代码在SAP C4C里创建Individual customer

    需求:使用nodejs代码在SAP Cloud for Customer里创建Individual customer实例. 代码: var createAndBind = require('../je ...

  7. NodeJS代码组织与部署

    使用NodeJS编写程序前,为了有个良好的开端,首先需要准备好代码的目录结构和部署方式,就如同修房子要先搭脚手架.本章将介绍与之相关的各种知识. 一.模块路径解析规则 我们已经知道,require函数 ...

  8. NPM 使用介绍(包管理工具,解决NodeJS代码部署上的很多问题)

    引用地址:http://www.runoob.com/nodejs/nodejs-npm.html NPM 使用介绍 NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问 ...

  9. nodejs学习笔记一——nodejs安装

    a.nodejs安装 nodejs的安装没有什么说的默认安装即可.安装包官网下载即可:nodejs官网 本人用的是window的安装包node-v4.2.6-x64.msi 安装完成后打开命令行查看使 ...

随机推荐

  1. maven控制台出现乱码

    maven默认环境为GBK,只需要改如下即可: 在IDEA中,打开File | Settings | Build, Execution, Deployment | Build Tools | Mave ...

  2. 手撸一个IOC容器

    IoC 什么是IoC? IoC是Inversion of Control(控制反转)的简称,注意它是一个技术思想.描述的是对象创建.管理的事情. 传统开发方式:比如类A依赖类B,往往会在类A里面new ...

  3. Redis高可用方案哨兵机制------ 配置文件sentinel.conf详解

    Redis的哨兵机制是官方推荐的一种高可用(HA)方案,我们在使用Redis的主从结构时,如果主节点挂掉,这时是不能自动进行主备切换和通知客户端主节点下线的. Redis-Sentinel机制主要用三 ...

  4. 大一C语言学习笔记(11)---编程篇--写一个程序,可以获取从键盘上输入的的三个数,并能够判断是否可以以这三个数字作为边长来构成一个三角形,如果可以的话,输出此三角形的周长及面积,要求 0 bug;

    考核内容: 写一个程序,可以获取从键盘上输入的的三个数,并能够判断是否可以以这三个数字作为边长来构成一个三角形,如果可以的话,输出此三角形的周长及面积: 答案: #include<stdio.h ...

  5. React-Router基础(<HashRouter>)

    <Router>使用URL(即window.location.hash)的哈希部分来保持UI与URL同步的A. 重要说明:哈希历史记录不支持location.key或location.st ...

  6. 大爽Python入门教程 0-4 安装Pycharm

    大爽Python入门公开课教案 点击查看教程总目录 安装重量级IDE--Pycharm 一 下载 下面步骤1,2中网络卡顿的朋友, 请直接前往步骤3来下载. 使用搜索引擎搜索Pycharm, 打开搜索 ...

  7. [cf1219G]Harvester

    分类讨论(以下仅考虑行,列的情况):1.4行的,求出每一行的和后找到4个最大值即可:2.3行1列,枚举列,再将每一行最大值减去那一列的值后取3个最大值得和即可:3.2行2列,发现行和列是等价的,因此可 ...

  8. [nowcoder5668H]Sort the Strings Revision

    考虑对于$p_{i}=0$,那么可以快速比较出$s_{0},s_{1},...,s_{i-1}$与$s_{i},s_{i+1},...,s_{n}$之间的大小关系,然后对两边分别找到最小的$p_{i} ...

  9. 拨开由问题《linux下malloc最大可申请的内存》带来的重重疑云

    今天阅读相关书籍的时候看到 "进程中堆的最大申请数量" 这一问题,我们知道使用malloc分配内存是在堆Heap里面分配的,如果一台机器一共有8GB物理内存,空闲5GB,那么我们使 ...

  10. [源码解析] PyTorch 分布式(13) ----- DistributedDataParallel 之 反向传播

    [源码解析] PyTorch 分布式(13) ----- DistributedDataParallel 之 反向传播 目录 [源码解析] PyTorch 分布式(13) ----- Distribu ...