node.js 启动流程解析

该版本是 node.js 的 Version 13,通过 VS Code 一步一步调试,大致了解了 node 的启动流程,同时也对 C++ 的语法和使用有了一些了解

从入口开始

image-20200317203257553

node_main.cc 文件中,这一行就是本次旅程的起点,node::start 启动并将 argc ( node 接收到的参数总数)和 argv (参数地址)传入。

InitializeOncePerProcess

image-20200317204723671

node.cc文件中,这行是开始初始化进程,从名字可以看出,这里保证只执行一次

image-20200317204915051

atexit 函数,从命名可以知道这个是在程序即将结束时会被触发,resetStdio 函数用来对标准 I/O 资源的回收,包括文件描述符 fdtty 相关的。

image-20200317205616775

PlatformInit

PlatformInit 函数,主要是对不同平台进行兼容并做一些平台相关的初始化工作,例如这里就是对文件描述符 fd 的初始化,确保进程中 fd 0(stdin)、1(stdout)、2(stderr) 是正常的, fstat 会返回文件的相关信息,535 行的作用是保证 fd0 1 2 的值都已经被占用,fd 分配从 3 开始,fd 表示的是进程能打开的文件数,系统默认是 1024

image-20200317211659671

定义并初始化 act 变量,根据系统的 signal 值,对不同的 signal 注册触发函数

image-20200317211844333

通过 fcntl 函数读取文件的文件描述符标记,fcntl 的文件状态标志总共有 7 个:O_RDONLY , O_WRONLY , O_RDWR , O_APPEND , O_NONBLOCK , O_SYNC 和 O_ASYNC

image-20200317212218589

RegisterSignalHandler 注册系统信号的触发函数,上面的是进程结束时的信号,SIGINT 当我们敲 c t r l + c 时会发出,SIGTERM 该信号可以被阻塞,等待合适时机再进行退出处理,而 SIGKILL 信号则是不能被捕获的,一旦发出则程序一定会退出,例如 kill -9 命令就会发出该信号

image-20200317212903968

getrlimit 是用来获取系统限定资源信息的函数, RLIMIT_NOFILE 的意思就是获取最大的文件描述符数,分为软限制和硬限制,软限制的值不能大于硬限制,这里利用二分搜索找出系统能设置的最大值

uv_setup_args

image-20200317214628489

uv_setup_args 的作用是复制一份命令行的参数,用于赋值给 process.title

InitializeNodeWithArgs

image-20200317214915223

RegisterBuiltinModules

image-20200317215110070

node_biding.cc

image-20200317215200324

image-20200317215631787

image-20200317215708278

image-20200317215751443

RegisterBuiltinModules 注册内置模块,使用了宏的写法,这些模块就是在 no de_XX.cc 中的模块,生成一个 modlist_internal 链表中借助 nm_link 指针指向下一个模块

image-20200317220638559

image-20200317220705582

image-20200317222210705

接下来是一些 Node 环境变量的配置

image-20200317222726981

把命令行中的配置项,例如 或者 - 开头的从 argv 分离到 exec_argv

InitializeCodeCache

image-20200317222941629

node_code_cache.cc

image-20200317223125934

这一步把提前编译好的 lib 下的 js 文件都存放在 code_cachemap 中,免去了读入文件的过程,加快启动速度

image-20200317223538497

image-20200317223643186

如果使用了 openssl 则使用 ca 证书,借助 BIO 进行读取

per_process::v8_platform.Initialize

node_v8_platform-inl.h

image-20200318084754293

node_platform.cc

image-20200318085218071

初始化 V8 平台相关的准备工作,包括添加运行状态追踪,生成 node 平台实例,配置多线程的线程池资源

v8::V8::InitializePlatform

api.cc

image-20200318085548818

v8.cc

image-20200318090015021

初始化 V8 平台,包括调用栈追踪和调用栈等功能的初始化

node 的代码中很多 v8::XXv8::internal::XX 的转换,等于是在中间加多了一层,对外暴露的 APIv8::XX,而 v8::internal::XX 则是内部实现,这样可以将代码分离,内部的变更都不会影响外部 API

V8::Initialize

node.cc

image-20200318090716146

v8::internal::V8::InitializeOncePerProcess

v8.cc

image-20200318090850322

image-20200318090915722

image-20200318091604665

image-20200318091202022

这里从命名就可以看出是只会执行一次的逻辑,类似的变量均被定义为 OnceType 类型,该类型的实质是 C++ 中的 std::atomic,该类型是用来多线程中避免竞争的原子操作原语,而 init_once 则是 OnceType 类型

loadstore 是读写值时使用的函数, Memory Order 意思是内存顺序,是 CPU 对指令执行优化引申的一个概念,多线程的同步机制一般有 mutexatomic,前者性能消耗大于后者。

once.cc

image-20200318092310287

此处的 memory_order_acquire 对应的是 release-acquire 机制,保证了线程读写对其他线程的可见性,这里我们可以看到 callOnceImpl 的实现,如果 once 的状态为已完成则直接返回,否则初始化一个 expected 变量赋值为初始态compare_exchange_strong 的作用就是做一次 CAS(compare and swap) 如果变量的值符合预期,即 初始态 ,那么就更新变量的状态为 执行中 ,执行 init_func 结束后,将变量状态设置为 结束态 ,如果 CAS 操作没有成功,且此时状态为 执行中 ,就调用sched_yield 交出执行权,这里的机制类似 JSgenerator

V8::InitializeOncePerProcessImpl

v8.cc

image-20200318093028160

image-20200318093241835

这里主要是做了各种初始化操作,包括 OSIsolate 的线程标识 ,还注册了一系列 v8 扩展

至此 node::Start 中的 InitializeOncePerProcess 完成

Node::Start

node.cc

image-20200318093809923

初始化 params 用于 Isolate 的初始化,然后获取 snapshot(快照) ,这是 Node 引入的加快启动速度的优化,将 IsolateContext 的部分初始值序列化后嵌入程序中,然后后续的初始化可以复用他们之前的 snapshot

main_instance

image-20200318100709188

生成 main_instance 实例,将 Isolateparamslibuv ,先前生成的 v8_platform ,命令行参数 args ,带 - 的参数 exec_args 传入

node_main_instance.cc

image-20200318101034652

isolate.cc

image-20200318101518634

image-20200318101859170

71 行 为 Isolate 初始化做前期工作,在第 2819 行初始化 Isolate 实例,isolatedata 保存了 Isolate 数据,isolateallocator 保存了 Isolate 的内存地址,接着初始化了线程管理器 ThreadManager ,最后初始化了微任务队列 MicrotaskQueue 这是一个双向循环链表

75 行在 platform 中注册 Isolate ,将该 Isolateeventloop 的信息记录在 map 中,便于多 Isolate 条件下的查找

76 行根据 params 的值设置 nodeheap 堆内存资源额度,包括新生代,老生代,semispace

77 行真正初始化 Isolate,根据 paramsIsolate 赋值

api.cc

image-20200318104708266

这里设置了堆栈的资源额度限制真正开始生效,第 8224 行从 snapshot 中获取部分数据赋值给 Isolate

image-20200318105229316

isolate_scope 负责记录 isolate 被线程访问的情况,通过 EntryStackItem 记录

至此,Isolate 的初始化正式完成

image-20200318105815473

保存一份 IsolateDataNodeMainInstance

NodeMainInstance::Run

image-20200318105932279

启动的前期工作准备就绪,这时候开始运行 main_instance

image-20200318110142682

HandleSope 经常能在代码中看到,这等于是开拓了一片作用域,在这之后的Local 变量都会被纳入 V8 的内存管理,在代码运行结束后资源能够被 GC 回收,这是一个简化的操作,实现原理是通过生成实例时调用 enter 方法,然后再运行结束时 C++会自动调用析构函数,这时候调用 exit 方法,起到了自动管理的作用

NodeMainInstance::CreateMainEnvironment

image-20200318110708041

image-20200318110815452

这里根据 snapshot 初始化 context ,下面的 context_scope 作用同上,其中的context 会被注入到 JS 中作为全局的 global 对象

image-20200318111731346

生成指向 Environment 的指针,这个时候会实例化 Environment,然后生成 node 中的 processprimordials 对象,准备注入到 Js

接下来开始初始化 libuv 和 诊断相关的逻辑

image-20200318114432240

注册不同阶段的 handle

Environment::RunBootstrapping

image-20200318114840841

image-20200318122035496

运行 JS 代码,lib/internal 中的 loadersnode

image-20200318115113607

FIXED_ONE_BYTE_STRING 作用是将 string 转换为 Local

运行 internal/bootstrap/loaders 文件的逻辑,同时注入 processgetLinkedBindinggetInternalBindingprimordials,这样在 Js 文件内部就可以访问到

loader_exports 返回的是 JS 文件中 returnloader_exports 对象,包括 require 方法, internalBinding 方法

EscapableHandleScope 的作用是用来要把内部的 Local 类型往外返回的时候,避免被 v8 回收

image-20200318115840921

image-20200318115900680

执行 ExecuteBootstrapper 中的 NativeModuleEnv::LookupAndCompile 拿到 JS 的内容后,调用 ->Call 进行调用

image-20200318120141916

image-20200318120315197

image-20200318120639781

LoadBuiltinModuleSource 中根据 Id ,即路径名查询, source_ 中是提前嵌入的 lib 下的 Js 文件,如果命中则直接将对应的字符串赋值给 source,然后将文件的基本信息保存在 origin

利用 ScriptCompiler::CompileFunctionInContext 注入 Context然后缓存到 codecache

image-20200318121845353

注入 process.env

至此 env->RunBootstrapping() 完成,CreateMainEnvironment 结束

image-20200318122321385

image-20200318122509396

调用 LoadEnvironment 装载环境,根据不同的命令行参数运行不同的 JS文件,一般情况下,internal/main/run_main_module

image-20200318122657817

image-20200318122751188

最后运行 uv_run ,启动 event loop,如果没有 active handle ,那么就准备释放资源和退出程序