Child process
一些基础性的介绍在这里。child_process
有两个核心方法,spawn
和fork
,二者唯一的区别是后者会在创立子进程的同时建立一个IPC通道,因此就分析fork
好了。本文将只讨论unix下的实现。
fork
unix下有一个fork
函数可以用来创建子进程。文档里有这么一句话:
Note: Unlike the fork(2)
POSIX system call, child_process.fork()
does not clone the current process.
这句话给人一种child_process
和unix的fork
没关系的错觉,像是libuv自己实现了一套fork
方法一样。实际上,child_process.fork
最终调用的就是libuv里的deps/uv/src/unix/process.c: uv_spawn()
,最终还是调用了系统的fork
. 而文档里说的没有克隆原始进程,意思应该是copy-on-write机制。
IPC channel
IPC在unix下有很多种实现方式,node使用的是unix domain socket方案。相较针对网络通讯设计的socket,效率更高。
unix下创建unix domain socket的系统函数是socketpair
,它将返回一对文件描述符fd[0] fd[1]
。对其中任意一个执行写操作之后,对这个文件描述符的读取操作会阻塞,只可以在另一个上读出数据。它实际上是一个全双工的IPC。而使用管道的话是单工的,双工通信需要创建两次,比较繁琐。
node在调用fork
操作的时候,会通过环境变量NODE_CHANNEL_FD
传递给子进程一个值作为IPC频道的暗号。我之前有参考过CNODE论坛上的帖子,看完这个帖子我有一个最大的疑问:fork
操作后,子进程是会完整的继承原始父进程打开的文件描述符的,所以如果在fork
之前调用了socketpair
,子进程是有这个文件描述符的,又何必要用NODE_CHANNEL_FD
折腾一大圈呢?
child_process.fork
最终调用的是libuv里的uv_spawn
, 来仔细分析一下uv_spawn
这个函数吧。第一个关键函数是uv__process_init_stdio
,它内部调用了uv__make_socketpair
,而该函数又调用了系统层面的socketpair
生成一对fd,这里没什么疑问。在生成socketpair
后,有这样一段代码:
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds))
return -errno;
uv__cloexec(fds[0], 1);
uv__cloexec(fds[1], 1);
这个uv__cloexec
将新生成的两个fd配置为exec
调用后即关闭,即close on exec. 这里是