深入出不来nodejs源码——流程总览

发表于:2018-6-19 13:12

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:QH-Jimmy    来源:51Testing软件测试网采编

  花了差不多两周时间过了下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,然后退出整个进程。



上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号