Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node源码粗读(1):一个简单的nodejs文件从运行到结束都发生了什么 #5

Open
xtx1130 opened this issue Oct 23, 2017 · 13 comments

Comments

@xtx1130
Copy link
Owner

xtx1130 commented Oct 23, 2017

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 小菜

@sunstdot
Copy link

厉害厉害.jpg; 坐等更新

@xtx1130
Copy link
Owner Author

xtx1130 commented Oct 24, 2017

暂时停更,今天不小心rm -rf 删掉了测试用的node源码。本来准备下一份最新的代码继续解析,但是master分支以及node-v8.x出现 ./configue --debug 报错问题,无法生成调试用的node应用程序。详情请查看 issue 待pr merge到master后开更

@xtx1130
Copy link
Owner Author

xtx1130 commented Oct 26, 2017

已更完。

@nickleefly
Copy link

你好,make -j 4生成./node之后怎么进入断点调试?

@xtx1130
Copy link
Owner Author

xtx1130 commented Nov 10, 2017

@nickleefly configure 的时候加上 --debug参数,这样在make的时候会生成out/Debug文件夹,选取out/Debug/node做为可执行文件进行调试,参数就是你要跑的js文件(相当于out/Debug/node xx.js) ,之后在node源码文件中打断点并进入ide的debug模式就好了

@nickleefly
Copy link

@xtx1130 谢谢 👍

@xtx1130
Copy link
Owner Author

xtx1130 commented Nov 10, 2017

@nickleefly 不客气😆

@nickleefly
Copy link

你好,Executable选out/Debug/node image
program arguments 写了test.js,然后编辑器菜单Run->Debug或者Run->Build,
编辑器的Console都是显示Build failed

@xtx1130
Copy link
Owner Author

xtx1130 commented Nov 13, 2017

@nickleefly 去掉Before launch Build 一栏中的Build

@nickleefly
Copy link

@xtx1130 在单位电脑才有CLion编辑器,可以了,谢谢 👍

@xtx1130
Copy link
Owner Author

xtx1130 commented Nov 17, 2017

@nickleefly 不客气😆

@xushuwei202
Copy link

你好,我想知道为什么我lib文件下所有js下断点都无效,只有test.js下断点有效

@xtx1130
Copy link
Owner Author

xtx1130 commented May 7, 2018

@xushuwei202 看下这篇文章#14

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants