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这里搭一个断点,看看接下来都会发生什么。
1. argc和argv
这两个在之后的代码里会经常见到,argc表示的是命令行参数个数,而argv表示的是命令行的参数,如果没有参数那么就是node的运行的绝对地址
2.platformInit运行
node运行的时候首先要走的便是platformInit方法,这个方法主要是对运行平台的一些参数进行初始化: 可以看到:首先有对信号的处理(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:
- CallOnceImpl对v8进行初始化的时候需要调用如下api: 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
环境搞定了,接下来就是开始运行咯。下面我们来看看这个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的列表:
- 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);这个从字面意思上来看是对程序的致命错误进行处理,没有细看,感觉可以直接过。
- ,这里贴一下图片,这样还能清晰看到边上clion的备注。Locker主要的作用是对当前的scope添加互斥锁。exit_code = Start 这里的Start有很多玄机,接下来我们单独开一章节详细剖析一下。
三、node执行过程中发生了什么
1.node::Environment::Start
这个函数对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 小菜
厉害👍
来自酷炫的 CNodeMD
我想知道ide的配置和node编译的过程,嘿嘿
@shay-an 谢谢😆
@dlyt ide用的clion,如果有edu邮箱的话可以一直免费使用,node编译过程就是平常的编译过程,configure --debug --prefix=projectDir,然后make clion主要就是用来设断点跟踪调试用
想问一下node源码怎么调试呢,最近也想看看node源码
@SunShinewyf 看下四楼的回复😆
是迄今为止最为详细的源码解析,赞叹!另外要读懂node的胶水层代码需要c++知识,这让我非常头疼。