深入出不来nodejs源码-流程总览
花了差不多两周时间过了下primer C++5th,完成了《C++从入门到精通》。(手动滑稽)
这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造成直接阅读上的不方便。
有些宏感觉真是一点鸟用都没有,比如说:
#define LIKELY(expr) expr
#define UNLIKELY(expr) expr
#define PRETTY_FUNCTION_NAME ""
这玩意翻译成JS大概就是:
const LIKELY = (expr) => expr;
JS中有些情况确实是需要这么一个函数,比如vue组件中的data,主要是为了防止复杂类型的引用问题。但是在C++我就不明白了,深拷贝简直不要太简单,可能这里为了强行语义化吧。
不过好在源码的规范超级棒,比如说:
1、内置常量宏都是全大写多单词下划线连接:FILE_TYPE_UNKNOWN代表无法识别的文件类型,对应16进制数字0x0000
2、函数名基本上能猜到用处:IsWindow7OrGreater函数判断操作系统是不是win7版本以上
3、注释随处可见
4、代码结构划分非常清晰,部分看不懂(或者不想看)的可以直接跳过,不影响后续内容理解
另外IDE也好使,直接帮我把LINUX系统的兼容代码直接置灰了,跳转大部分情况也很好用。
本来打算从简单一点的API来讲,比如说require方法是如何运作的,但是这东西过于简单,百度一搜一大把,已经是被人写烂的内容,所以不打算开这个坑(更重要的是那玩意是JS,如何对得起我学了两周C++)。
因此我决定开一个目录坑,总览一下node从启动到运行,哪些部分我能看懂,理一理。
正文开始
声明:本文目前基于node-v10.1.0,未来如有更改会说明。代码来源于官网下载的源码压缩包,详情见上一篇文章。
首先上个图瞧一眼大概的流程:
是不是很少很简单?对啊,因为我只能看懂这么多……
开始吹。
首先每个C++项目都有一个启动项目,每个项目里有一个主函数,当初我以为只能叫main,后来发现wmain也是OK的。
而在node中,wmain是windows系统的主函数,main是LINUX系统的主函数。区分系统的关键在于一个特定的宏,windows是_WIN32,这个宏会被自动定义,相关源码如下:
#ifdef _WIN32
int wmain(int argc, wchar_t *wargv[]) {
// ...
return node::Start(argc, argv);
}
#else
// UNIX
#ifdef __linux__
int main(int argc, char *argv[]) {
// ...
}
这里就不扯什么头文件了,主函数会调用node模块的Start方法,node模块文件名是node.cc,相当于项目的主文件,从这里开始,也从这里结束。
从上面可以看到,这个函数有三大步(能看懂的),非常简单粗暴,一个一个讲。
Init
这个函数相关源码如下:
void Init(int* argc,
const char** argv,
int* exec_argc,
const char*** exec_argv) {
// Initialize prog_start_time to get relative uptime.
prog_start_time = static_cast<double>(uv_now(uv_default_loop()));
// 加载内置模块
RegisterBuiltinModules();
// Make inherited handles noninheritable.
uv_disable_stdio_inheritance(); // 后面还有很多代码 但是我看不懂 流下了没技术的眼泪……
}
需要关注的只要那个RegisterBuiltinModules方法,从名字也可以看出来,就是加载内置模块,描述有误,下一节修正。
这个函数的定义也很奇妙,简直就是宏函数,如下:
void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
NODE_BUILTIN_MODULES(V)
#undef V
}
函数的声明相信学JS的也能看懂是什么,主要是函数内容很奇怪,竟然是一个宏定义。
用JS翻译一下那个宏,意思大概就是:
// C++:#define V(modname) _register_##modname();
const V = (modname) => `_register_${modname}`();
当然,这个鸟代码是不可能执行的,只是为了方便理解。
打个比方就能明白了,假如我调用了V(a),那么在执行的时候,实际上调用的是_register_a这个方法,双警号只是一个字符串拼接。
好,解决了宏问题,可以看下这个NODE_BUILTIN_MODULES是什么函数了。然而,这也是一个宏,内容如下:
#define NODE_BUILTIN_MODULES(V) \
NODE_BUILTIN_STANDARD_MODULES(V) \
NODE_BUILTIN_OPENSSL_MODULES(V) \
NODE_BUILTIN_ICU_MODULES(V)
看到这闪亮亮的格式,应该可以猜到,这三个东西还是宏!!!
好在宏不过三代,随便点一个跳转就会发现,实际上这些宏都是为了调用具体的模块加载方法,比如:
#define NODE_BUILTIN_STANDARD_MODULES(V) \
V(async_wrap) \
V(buffer) \
// 还有很多比如fs、url等
这样把最初的函数简单转换一下,大概就是:
void RegisterBuiltinModules() {
_register_async_wrap();
_register_buffer();
// 等等
}
至于这些方法的内容是什么?地点在哪?我还没找到。
Start
V8引擎的加载就先忽略,触及到我的知识盲区。最后看一下那个内联Start函数,这个方法主要完成三件事:
1、初始化并加载全局变量global
2、加载辅助工具
3、事件轮询
其中1、2两个都是在同一个函数中执行的,即LoadEnviroment,上一些关键的源码证明下:
void LoadEnvironment(Environment* env) {
// ... Local<String> loaders_name =
FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
Local<Function> loaders_bootstrapper =
GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
// 还有"internal/bootstrap/node.js" // 生成全局global对象的引用
Local<Object> global = env->context()->Global(); // ... // 设置全局对象
global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global); // ... // 生成函数参数
Local<Value> loaders_bootstrapper_args[] = {
env->process_object(),
get_binding_fn,
get_linked_binding_fn,
get_internal_binding_fn
}; // 加载辅助函数
Local<Value> bootstrapped_loaders;
if (!ExecuteBootstrapper(env, loaders_bootstrapper,
arraysize(loaders_bootstrapper_args),
loaders_bootstrapper_args,
&bootstrapped_loaders)) {
return;
} // 加载Bootstrap Node.js
}
这里通过Environment类生成了global对象,然后挂载到全局。
辅助函数则是加载了internal/bootstrap中的两个JS文件,加载的时候参数传入了C++代码生成的特殊对象,配合对应的JS源文件就很好理解:
'use strict';
// internal/bootstrap/loader.js
(function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,getInternalBinding) {
// ...模块内容
});
这个JS文件内容看起来只是一个函数定义,但是实际上在启动后已经执行完了,四个参数是通过上述C++代码注入的,依次对应上面的4个东西。
这里有一个小地方稍微讲讲吧,如果在启动时已经执行完了,那么看一下下面的代码:
// internal/bootstrap/loader.js
const { NativeModule } = require('internal/bootstrap/loaders');
这是node中调用require方法的入口JS文件,理论上引入的应该是上面那个函数,不可能得到NativeModule对象的。
不过node的require并不像webpack那么简单粗暴,readFile + parse直接得到文件输出对象,而是区分了内部模块与外部模块。对于内部模块,有一套新的逻辑,相关代码如下:
// require => _load
// 判断传入的字符串是否是内部模块名
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
// 调用另一套逻辑代码
return NativeModule.require(filename);
}
而NativeModule就是一开始加载过的辅助工具JS,涉及到的代码如下:
const loaderExports = { internalBinding, NativeModule };
const loaderId = 'internal/bootstrap/loaders';
NativeModule.require = function(id) {
// Do not expose this to user land even with --expose-internals
// 对此require进行特殊处理
if (id === loaderId) {
return loaderExports;
}
// ...
}
可以看到在require的地方做了特殊处理,会直接返回指定的对象,至于这两个对象是什么,后面再慢慢讲吧。
值得注意的是那个注释,这个字符串十分特殊,node并不希望用户获取该模块,因为得到的对象拥有直接调用底层C++代码的能力,十分危险。
完成准备工作后,就开始了事件轮询跑进程,相关代码如下:
{
SealHandleScope seal(isolate);
// 循环控制参数
bool more;
// 标记事件轮询开始
env.performance_state()->Mark(
node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START);
// 开始event_loop
do {
uv_run(env.event_loop(), UV_RUN_DEFAULT);
v8_platform.DrainVMTasks(isolate);
more = uv_loop_alive(env.event_loop());
if (more)
continue;
RunBeforeExit(&env);
// Emit `beforeExit` if the loop became alive either after emitting
// event, or after running some callbacks.
more = uv_loop_alive(env.event_loop());
} while (more == true);
// 标记事件轮询结束
env.performance_state()->Mark(
node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
}
事件机制的实现在第三方依赖模块uv当中,轮询过程则是这里的do-while循环。
一旦node轮询结束,会返回一个exit_code,然后退出整个进程。
完结,下一节写啥还得再想想。
深入出不来nodejs源码-流程总览的更多相关文章
- 深入出不来nodejs源码-V8引擎初探
原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...
- 深入出不来nodejs源码-编译启动(1)
整整弄了两天,踩了无数的坑,各种奇怪的error,最后终于编译成功了. 网上的教程基本上都过时了,或者是版本不对,都会报一些奇怪的错误,这里总结一下目前可行的流程. node版本:v10.1.0. 首 ...
- 深入出不来nodejs源码-timer模块(JS篇)
鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看. 其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单 ...
- 深入出不来nodejs源码-从fs.stat方法来看node架构
node的源码分析还挺多的,不过像我这样愣头完全平铺源码做解析的貌似还没有,所以开个先例,从一个API来了解node的调用链. 首先上一张整体的图,网上翻到的,自己懒得画: 这里的层次结构十分的清晰, ...
- 深入出不来nodejs源码-events模块
这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼. 学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定.这个东西贯穿了整个JS ...
- 深入出不来nodejs源码-timer模块(C++篇)
终于可以填上坑了. 简单回顾一下之前JS篇内容,每一次setTimeout的调用,会在一个对象中添加一个键值对,键为延迟时间,值为一个链表,将所有该时间对应的事件串起来,图如下: 而每一个延迟键值对的 ...
- 深入出不来nodejs源码-内置模块引入再探
我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...
- 深入出不来nodejs源码-内置模块引入初探
重新审视了一下上一篇的内容,配合源码发现有些地方说的不太对,或者不太严谨. 主要是关于内置模块引入的问题,当时我是这样描述的: 需要关注的只要那个RegisterBuiltinModules方法,从名 ...
- Flask 源码流程,上下文管理
源码流程 创建对象 from flask import Flask """ 1 实例化对象 app """ app = Flask(__na ...
随机推荐
- 使用UTL_HTTP时遭遇ORA-29273
http://blog.itpub.net/8520577/viewspace-1295182/ 项目中需要使用utl_http访问webserivce.使用utl_http时报错ORA-29273. ...
- iOS 百度地图截屏
关于百度地图截屏的问题,发现不能用常用的方法进行载屏,常用的截屏方法所得到的图片地图瓦片底图会显示空白,网上给出的答案是这样的 :因为百度地图不是用UIKit实现的,所以得不到截图! 不过通过Open ...
- NODE-WEBKIT教程(5)NATIVE UI API 之FRAMELESS WINDOW
node-webkit教程(5)Native UI API 之Frameless window 文/玄魂 原文链接:http://www.xuanhun521.com/Blog/2014/4/15/n ...
- C#调用接口注意要点
在用C#调用接口的时候,遇到需要通过调用登录接口才能调用其他的接口,因为在其他的接口需要在登录的状态下保存Cookie值才能有权限调用, 所以首先需要通过调用登录接口来保存cookie值,再进行其他接 ...
- 利用adb 打开手机应用程序
通过adb打开android应用,我们需要做的第一步,就是查看当前app的入口,第二部,就是通过命令启动我们的app入口 查看app的启动画面 在运行下面命令时,先启动想要通过adb打开的app #查 ...
- Linux系统文件与目录管理(1)
文件与目录的操作 在Linux系统的文件与目录的管理上,不外乎『显示属性』.『拷贝』.『删除文件』.『移动文件或目录』.『重命名』等常用操作,由于文件与目录的管理在 Linux当中是很重要的,尤其是每 ...
- Django(框架、模板)
day65 参考:https://www.cnblogs.com/liwenzhou/p/8296964.html Django框架的设计模式借鉴了MVC框架的思想,也是分成三部分,来降低各个部分之间 ...
- D17——C语言基础学PYTHON
C语言基础学习PYTHON——基础学习D17 20181014内容纲要: 1.jQuery介绍 2.jQuery功能介绍 (1)jQuery的引入方式 (2)选择器 (3)筛选 (4)文本操作 (5) ...
- JPA-style positional param was not an integral ordinal
参数错误 多为SQL语句问题 例如SQL拼装中的空格,换行时首位应多加空格保持语句效果
- web工程迁移---在一个jboss5或jboss6中运行多个实例
在工作中遇到的,如何在一个jboss中运行多个节点(segment). 我使用的环境是win7.jboss5.jboss6.JDK6 1.jboss5下运行多个实例 第一步不用说,首先要在环境变量中设 ...