我们知道nodejs是单进程(单线程)的,但是nodejs也为用户实现了多进程的能力,下面我们看一下nodejs里多进程的架构是怎么样的。 nodejs提供同步和异步创建进程的方式。我们首先看一下异步的方式,nodejs创建进程的方式由很多种。但是归根到底是通过spawn函数。所以我们从这个函数开始,看一下整个流程。
var spawn = exports.spawn = function(/*file, args, options*/) {
var opts = normalizeSpawnArguments.apply(null, arguments);
var options = opts.options;
var child = new ChildProcess();
debug('spawn', opts.args, options);
child.spawn({
file: opts.file,
args: opts.args,
cwd: options.cwd,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
detached: !!options.detached,
envPairs: opts.envPairs,
stdio: options.stdio,
uid: options.uid,
gid: options.gid
});
return child;
};
我们看到spawn函数只是对ChildProcess函数的封装。然后调用他的spawn函数(只列出核心代码)。
const { Process } = process.binding('process_wrap');
function ChildProcess() {
EventEmitter.call(this);
this._handle = new Process();
}
ChildProcess.prototype.spawn = function(options) {
this._handle.spawn(options);
}
ChildProcess也是对Process的封装。Process是js层和c++层的桥梁,我们找到他对应的c++模块。
NODE_BUILTIN_MODULE_CONTEXT_AWARE(process_wrap, node::ProcessWrap::Initialize)
即在js层调用process.binding('process_wrap')的时候,拿到的是node::ProcessWrap::Initialize导出的对象。
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context
) {
Environment* env = Environment::GetCurrent(context);
// 定义一个构造函数,值是New
Local<FunctionTemplate> constructor = env->NewFunctionTemplate(New);
constructor->InstanceTemplate()->SetInternalFieldCount(1);
// 拿到一个字符串
Local<String> processString =
FIXED_ONE_BYTE_STRING(env->isolate(), "Process");
constructor->SetClassName(processString);
AsyncWrap::AddWrapMethods(env, constructor);
// 设置这个构造函数的原型方法
env->SetProtoMethod(constructor, "close", HandleWrap::Close);
env->SetProtoMethod(constructor, "spawn", Spawn);
env->SetProtoMethod(constructor, "kill", Kill);
...
/*
类似js里的module.exports = {Process: New}
js层new Process的时候会相对于执行new New
*/
target->Set(processString, constructor->GetFunction());
}
上面的代码翻译成js大概如下。
function New() {}
New.prototype = {
spawn: Spawn,
kill: Kill,
close: Close
...
}
module.exports = {Process: New}
所以new Process的时候,执行的是new New。
static void New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
new ProcessWrap(env, args.This());
}
ProcessWrap(Environment* env, Local<Object> object)
: HandleWrap(
env,
object,
reinterpret_cast<uv_handle_t*>(&process_),
AsyncWrap::PROVIDER_PROCESSWRAP
)
{
}
我们看到new New就是new ProcessWrap,但是New函数没有返回一个值。继续往下看。
HandleWrap::HandleWrap(...) {
Wrap(object, this);
}
void Wrap(v8::Local<v8::Object> object, TypeName* pointer) {
object->SetAlignedPointerInInternalField(0, pointer);
}
v8的套路有点复杂,大致就是在FunctionCallbackInfo对象里保存了ProcessWrap类的对象。后续调用的时候会取出来。然后给js返回一个对象。接着
this._handle.spawn(options);
这时候会执行
static void Spawn(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
ProcessWrap* wrap;
/*
取出刚才的ProcessWrap对象
wrap = args->GetAlignedPointerFromInternalField(0);
*/
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
int err = uv_spawn(env->event_loop(), &wrap->process_, &options);
args.GetReturnValue().Set(err);
}
接着我们通过uv_spawn来到了c语言层。uv_spawn总的来说做了下面几个事情。 1 主进程注册SIGCHLD信号,处理函数为uv__chld。SIGCHLD信号是子进程退出时发出的。 2 处理进程间通信、标准输入、输出。 3 fork出子进程 4 在uv_process_t结构图中保存子进程信息,uv_process_t是c++层和c层的联系。 5 把uv_process_t插入libuv事件循环的process_handles队列 6 主进程和子进程各自运行。 整个流程下来,大致形成如图所示的架构。
在这里插入图片描述 当进程退出的时候。nodejs主进程会收到SIGCHLD信号。然后执行uv__chld。该函数遍历libuv进程队列中的节点,通过waitpid判断该节点对应的进程是否已经退出后,从而收集已退出的节点,然后移出libuv队列,最后执行已退出进程的回调。
static void uv__chld(uv_signal_t* handle, int signum) {
uv_process_t* process;
uv_loop_t* loop;
int exit_status;
int term_signal;
int status;
pid_t pid;
QUEUE pending;
QUEUE* q;
QUEUE* h;
// 保存进程(已退出的状态)的队列
QUEUE_INIT(&pending);
loop = handle->loop;
h = &loop->process_handles;
q = QUEUE_HEAD(h);
// 收集已退出的进程
while (q != h) {
process = QUEUE_DATA(q, uv_process_t, queue);
q = QUEUE_NEXT(q);
do
// WNOHANG非阻塞等待子进程退出,其实就是看哪个子进程退出了,没有的话就直接返回,而不是阻塞
pid = waitpid(process->pid, &status, WNOHANG);
while (pid == -1 && errno == EINTR);
if (pid == 0)
continue;
// 进程退出了,保存退出状态,移出队列,插入peding队列,等待处理
process->status = status;
QUEUE_REMOVE(&process->queue);
QUEUE_INSERT_TAIL(&pending, &process->queue);
}
h = &pending;
q = QUEUE_HEAD(h);
// 是否有退出的进程
while (q != h) {
process = QUEUE_DATA(q, uv_process_t, queue);
q = QUEUE_NEXT(q);
QUEUE_REMOVE(&process->queue);
QUEUE_INIT(&process->queue);
uv__handle_stop(process);
if (process->exit_cb == NULL)
continue;
exit_status = 0;
// 获取退出信息,执行上传回调
if (WIFEXITED(process->status))
exit_status = WEXITSTATUS(process->status);
term_signal = 0;
if (WIFSIGNALED(process->status))
term_signal = WTERMSIG(process->status);
process->exit_cb(process, exit_status, term_signal);
}
}
这就是nodejs中进程的整个生命周期。 接下来看看如何以同步的方式创建进程。入口函数是spawnSync。对应的c++模块是spawn_sync。过程就不详细说明了,直接看核心代码。
void SyncProcessRunner::TryInitializeAndRunLoop(Local<Value> options) {
int r;
uv_loop_ = new uv_loop_t;
// ExitCallback会把子进程的结构体从libuv中移除
uv_process_options_.exit_cb = ExitCallback;
r = uv_spawn(uv_loop_, &uv_process_, &uv_process_options_);
r = uv_run(uv_loop_, UV_RUN_DEFAULT);
}
我们看到,对于同步创建进程,nodejs没有使用waitpid这种方式阻塞自己,从而等待子进程退出。而是重新开启了一个事件循环。我们知道uv_run是一个死循环,所以这时候,nodejs主进程会阻塞在上面的uv_run。直到子进程退出,uv_run才会退出循环,从而再次回到nodejs原来的事件循环。