node源码粗读(1):一个简单的nodejs文件从运行到结束都发生了什么
发布于 6 年前 作者 xtx1130 5753 次浏览 来自 分享

os环境:macOS 10.12.5 ,ide:cLion,node版本:v8.0.0

一、配置ide和node编译

对ide的配置和node编译的过程这里不赘述了,如果有时间,可能写一篇blog简单介绍一下。

二、node运行入口粗读

node入口集中在文件 node/src/node_main.cc中,让我们在node::Start这里搭一个断点,看看接下来都会发生什么。 issue5

1. argc和argv

这两个在之后的代码里会经常见到,argc表示的是命令行参数个数,而argv表示的是命令行的参数,如果没有参数那么就是node的运行的绝对地址

2.platformInit运行

node运行的时候首先要走的便是platformInit方法,这个方法主要是对运行平台的一些参数进行初始化: issue5 可以看到:首先有对信号的处理(sig*),还有对系统的stdin、stdout的检测(STDIN_FILENO,STDERR_FILENO),这里截图没截全,就不一一介绍了。

3.调用node::performance::performance_node_start

这里从字面就很好理解,开始对node性能进行记录,这里面有个要注意的地方是:它底层其实调用的是uv__hrtime。node有很多可以计算时间的方法,大家如果读读源码,就会发现,所有计算耗时的方法都绕不开这个api。

4.调用uv_setup_args

uv_setup_args调用主要要解决的问题就是调用uv_malloc对argv的副本new_argv分配内存,并返回。说白了就是复制一份argv给process.title这个api用。

5.调用Init方法

Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);

大家可以看到这个Init有四个参数,argc和argv刚才已经介绍过了,而后面两个参数分别给argc和argv加上了exec前缀,经过阅读就会发现,exec前缀就是待执行状态的argc和argv。在Init函数体内,主要做的事情有以下几个:

  • 开始计算node程序运行时间
prog_start_time = static_cast<double>(uv_now(uv_default_loop()));
  • 通过调用uv_loop_configure,对libuv信号进行确认,包括:uv_loop_block_signal等
  • 通过node::SafeGetenv 读取node环境相关的argv来建立node环境相关配置参数的链接,例如:
SafeGetenv("OPENSSL_CONF", &openssl_config);

node建立起openSSL_CONF,而这个CONF来自你的参数–openssl-config

  • 通过node::ProcessArgv读取node参数相关的并进行分配内存处理,例如 --version,–trace-warnings等

6.判断openSSL

#if HAVE_OPENSSL
  {
    std::string extra_ca_certs;
    if (SafeGetenv("NODE_EXTRA_CA_CERTS", &extra_ca_certs))
      crypto::UseExtraCaCerts(extra_ca_certs);
  }

这里没什么好说的,就是判断openSSL,如果有的话读取ca,为用openSSL通讯做准备。

7. v8_platform.Initialize初始化

v8_platform.Initialize,这里主要对libuv线程池做初始化操作。

8.V8::Initialize()初始化

接线来就是重头戏了,对v8进行初始化操作,其中又囊括了很多点,我在下面一一列出,里面一些概念也会在列出的时候提及一下:

  • 首先会调用CallOnce(OnceType* once, void (*init_func)()),它会通过原子操作的方法(Acquire_Load)来确认是否已经调用过,如果没有调用过则进入到CallOnceImpl并最终调用 init_func,这个函数只有在初始化的时候进行调用:
// @files ./deps/v8/src/base/once.h line 83
inline void CallOnce(OnceType* once, NoArgFunction init_func) {
  if (Acquire_Load(once) != ONCE_STATE_DONE) {
    CallOnceImpl(once, reinterpret_cast<PointerArgFunction>(init_func), NULL);
  }
}

在这里有一个比较重要的概念,就是OnceType,这是node作者定义的一种类型,如果翻一下源码可以翻到一个宏定义 V8_DECLARE_ONCE,这是专门用来声明OnceType类型的,对于只需要在创建的时候声明一次的都会定义为OnceType,通过全局搜索就很容易找出来OnceType: issue5

  • CallOnceImpl对v8进行初始化的时候需要调用如下api: issue5 v8::internal::V8::InitializeOncePerProcessImpl,这个函数通过名字就能很好理解,就是只在进程运行时候初始化一次的接口,也就是上述的OnceType,咱们接着往下看。
  • 这个api里面首先调用的是base::OS::Initialize这个api,这里不做过多解释,就是对操作系统相关的东西做兼容初始化操作
  • 接下来比较重要的api就是Isolate::InitializeOncePerProcess() 这个了,这是对v8运行环境isolate进行初始化的地方(并没有调用生成isolate,只是告诉程序在进程运行的时候需要对isolate做的一系列操作),里面主要做的操作有:初始化isolate锁;初始化线程、线程数据块、数据表地址。
  • sampler::Sampler::SetUp(),采样器在这里就不做过多分析了,就是创建一个线程采集v8的状态。
  • 在init_func调用结束后会调用一下c++中的原子操作方法Release_Store(应该是针对于内存屏障进行原子写操作的一个api),将数据装到内存中指定位置:
inline void Release_Store(volatile Atomic64* ptr, Atomic64 value) {
  __atomic_store_n(ptr, value, __ATOMIC_RELEASE);
}

9.调用node::performance::performance_v8_start

node::performance::performance_v8_start = PERFORMANCE_NOW();

这似曾相识的代码就不过多解释了,就是对v8进行性能记录,底层同样调用的uv_hrtime。

10.调用内联Start

issue5 环境搞定了,接下来就是开始运行咯。下面我们来看看这个start都做了哪些事情
看到它第一个参数,就知道了,先初始化uv_default_loop,就不再过多解释了,有问题的可以翻翻libuv,很好理解,下面贴一下初始化的代码:

uv_loop_t* uv_default_loop(void) {
  if (default_loop_ptr != NULL)
    return default_loop_ptr;
  if (uv_loop_init(&default_loop_struct))
    return NULL;
  default_loop_ptr = &default_loop_struct;
  return default_loop_ptr;
}

接下来是真正的start了,在这里贴一下代码,然后详细解释一下:

 Isolate::CreateParams params;
  ArrayBufferAllocator allocator;
  params.array_buffer_allocator = &allocator;
#ifdef NODE_ENABLE_VTUNE_PROFILING
  params.code_event_handler = vTune::GetVtuneCodeEventHandler();
#endif

  Isolate* const isolate = Isolate::New(params);
  if (isolate == nullptr)
    return 12;  // Signal internal error.

  isolate->AddMessageListener(OnMessage);
  isolate->SetAbortOnUncaughtExceptionCallback(ShouldAbortOnUncaughtException);
  isolate->SetAutorunMicrotasks(false);
  isolate->SetFatalErrorHandler(OnFatalError);
  • 第一行应该比较好理解,就是在isolate中创建参数,下面是params的列表: issue5
  • ArrayBufferAllocator allocator这里是一个很重要的知识点,之前在Init的时候忘了提及了,node中buffer不会占用v8内存,而是开辟出的独立空间,这里就是声明之前Init时候封装好的ArrayBufferAllocator
  • 解释一下这句代码:
 isolate->AddMessageListener(OnMessage);

这句话表面上是给isolate添加监听,而如果你深入去研究的话就会发现,它是通过HandleScope来监听javascript对象内存的变化并发出相应的message。

  • 又是感觉似曾相识的代码,是不是经常在node中看到UncaughtException这几个字母,O(∩_∩)O
isolate->SetAbortOnUncaughtExceptionCallback(ShouldAbortOnUncaughtException);

这里要做的是对未捕获的异常进行监听(可能对libuv中的问题进行了监听,具体没有深入看,之后会进行确认)。

  • isolate->SetAutorunMicrotasks(false); 这个我就不做过多解释了,microtask和marcotask应该大家都比较明白,不明白的可以随便翻翻资料或者看看libuv的event loop,很好理解。
  • isolate->SetFatalErrorHandler(OnFatalError);这个从字面意思上来看是对程序的致命错误进行处理,没有细看,感觉可以直接过。
  • issue5,这里贴一下图片,这样还能清晰看到边上clion的备注。Locker主要的作用是对当前的scope添加互斥锁。exit_code = Start 这里的Start有很多玄机,接下来我们单独开一章节详细剖析一下。

三、node执行过程中发生了什么

issue5

1.node::Environment::Start

issue5 这个函数对node运行时候的环境做了初始化,其中初始化了大家非常熟悉的HandleScope,Context,下面还有对uv_idle_init以及uv_timer_init等初始化的操作,这里就不过多介绍了。

2.async_hooks和LoadEnvironment

接下来注意这段代码:

    env.async_hooks()->push_async_ids(1, 0);
    LoadEnvironment(&env);
    env.async_hooks()->pop_async_id(1);

这里引入了一个新的概念就是async_hooks,从这段代码就能看出来async_hooks是如何记录整个生命周期的,因为他是在生命周期之前推入,而又是在生命周期之后推出的。
LoadEnvironment则是整个运行时候的环境,接下来会讲解到LoadEnvironment到底都做了什么。

3.node::LoadEnvironment

void LoadEnvironment(Environment* env) {
  HandleScope handle_scope(env->isolate());
  TryCatch try_catch(env->isolate());
  try_catch.SetVerbose(false);
  Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
                                                    "bootstrap_node.js");
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
  if (try_catch.HasCaught())  {
    ReportException(env, try_catch);
    exit(10);
  }
  CHECK(f_value->IsFunction());
  Local<Function> f = Local<Function>::Cast(f_value);
  Local<Object> global = env->context()->Global();//创建Global
  //...
  try_catch.SetVerbose(true);
  env->SetMethod(env->process_object(), "_rawDebug", RawDebug);
  global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);
  Local<Value> arg = env->process_object();//创建process
  f->Call(Null(env->isolate()), 1, &arg);//通过bootstrap_node.js中的startup方法来调用本地node代码
}

在这里重点说一下很有名的bootstrap_node.js,这是外部js文件的入口,外部js文件进来之后要经过几个主要的步骤:通过vm.script 校验代码;通过preloadModules()预加载模块;通过module.rumMain()执行外部js文件。
在内存分配的时候,有个小知识点,正好在这里提一下:

bool large_object = size_in_bytes > kMaxRegularHeapObjectSize;
  HeapObject* object = nullptr;
  AllocationResult allocation;
  if (NEW_SPACE == space) {
    if (large_object) {
      space = LO_SPACE;
    } else {
      allocation = new_space_->AllocateRaw(size_in_bytes, alignment);
      if (allocation.To(&object)) {
        OnAllocationEvent(object, size_in_bytes);
      }
      return allocation;
    }
  }

大家可以注意一下这段代码,相信懂英文的人都能看懂,这里是对大对象进行判断,如果字节超过kMaxRegularHeapObjectSize则会被分配到LO_SPACE中。在这里给大家解释一下这个space,node中做堆内存分配的space细分总共五类,分别是:

  • LO_SPACE(存放大对象)
  • NEW_SPACE(能被copying collector复制收集法收集的内存,主要是未被回收过的,为什么叫复制收集请自行查阅Scavenge算法)
  • OLD_SPACE(主要存放的是对NEW_SPACE的指针以及NEW_SPACE迁移过来的对象)
  • CODE_SPACE(存放经过turbofan优化后的指令对象)
  • MAP_SPACE(主要用于存放map)
  • 还有游离于这几类之外的UNREACHABLE(容错处理,不做讨论)
    其中NEW_SPACE的内存是连续的,OLD_SPACE和MAP_SAPCE则是基于页进行管理的,存放不下的话会不断新加内存页进来,直到max_size。LO_SPACE则是有单独的存储空间,也是基于页进行管理(所以粗分其实只有三类)。 下面贴一部分OLD_SPACE内存分配代码:
int paged_space_count = LAST_PAGED_SPACE - FIRST_PAGED_SPACE + 1;
  initial_max_old_generation_size_ = max_old_generation_size_ =
      Max(static_cast<size_t>(paged_space_count * Page::kPageSize),
          max_old_generation_size_);
//其中 kPageSize 定义如下
class MemoryChunk{
  public:
    //省略
    static const int kPageSize = 1 << kPageSizeBits;//kPageSizeBits 与操作系统内存页大小有关
    //省略
}
class Page : public MemoryChunk{
//省略
}

还有一个知识点,就是node内置模块是如何加载的,在这里不做展开讨论了,网上资料很多,请自行查阅。

四、运行结束

运行结束之后的工作就做过多解释了,大家简单看下代码直接过了,无非是一些扫尾的工作:

if (trace_enabled) {
    v8_platform.StopTracingAgent();
  }
  v8_initialized = false;
  V8::Dispose();
  v8_platform.Dispose();

  delete[] exec_argv;
  exec_argv = nullptr;

五、总结

至此,整体流程也就大致清晰了,文章比较干,还是希望大家真正上手调试走一遍流程,看一下代码,这样印象才能更深刻,如果文章中有说的不正确的地方,也请大神在评论中进行指正。 by 小菜

7 回复

厉害👍

来自酷炫的 CNodeMD

我想知道ide的配置和node编译的过程,嘿嘿

@shay-an 谢谢😆

@dlyt ide用的clion,如果有edu邮箱的话可以一直免费使用,node编译过程就是平常的编译过程,configure --debug --prefix=projectDir,然后make clion主要就是用来设断点跟踪调试用

想问一下node源码怎么调试呢,最近也想看看node源码

@SunShinewyf 看下四楼的回复😆

是迄今为止最为详细的源码解析,赞叹!另外要读懂node的胶水层代码需要c++知识,这让我非常头疼。

回到顶部