子进程 (child_process)
child_process 是Node的一个十分重要的模块,通过它可以实现创建多进程,以利用单机的多核计算资源。虽然,Nodejs天生是单线程单进程的,但是有了child_process模块,可以在程序中直接创建子进程,并使用主进程和子进程之间实现通信。
进程通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核, 在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
类型 | 无连接 | 可靠 | 流控制 | 优先级 |
---|---|---|---|---|
普通PIPE | N | Y | Y | N |
命名PIPE | N | Y | Y | N |
消息队列 | N | Y | Y | N |
信号量 | N | Y | Y | Y |
共享存储 | N | Y | Y | Y |
UNIX流SOCKET | N | Y | Y | N |
UNIX数据包SOCKET | Y | Y | N | N |
- 注:无连接: 指无需调用某种形式的open,就有发送消息的能力流控制:
Node 中实现 IPC 通信的是管道技术,但只是抽象的称呼,具体细节实现由 libuv提供, 在 windows 下由命名管道(named pipe)实现, *nix 系统则采用 Unix Domain Socket实现。 也就是上表中的最后第二个。
Socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。
Depending on the platform, unix domain sockets can achieve around 50% more throughput than the TCP/IP loopback (on Linux for instance).
这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
创建子进程
- spawn()启动一个子进程来执行命令
- exec()启动一个子进程来执行命令, 带回调参数获知子进程的情况, 可指定进程运行的超时时间
- execFile()启动一个子进程来执行一个可执行文件, 可指定进程运行的超时时间
- fork() 与spawn()类似, 不同在于它创建的node子进程只需指定要执行的js文件模块即可
// don't call this example code var cp = require('child_process'); cp.spawn('node', ['work.js']); cp.exec('node work.js', function(err, stdout, stderr) { // some code }); cp.execFile('work.js', function(err, stdout, stderr) { // some code }); cp.fork('./work.js');
exec方法会直接调用bash(/bin/sh程序)来解释命令,所以如果有用户输入的参数,exec方法是不安全的。
var path = ";user input";
child_process.exec('ls -l ' + path, function (err, data) {
console.log(data);
});
上面代码表示,在bash环境下,ls -l; user input
会直接运行。如果用户输入恶意代码,将会带来安全风险。因此,在有用户输入的情况下,最好不使用exec方法,而是使用execFile方法。
建立 IPC 通道
父进程在创建子进程前创建IPC通道并监听, 用环境变量NODE_CHANNEL_FD告诉子进程的IPC的文件描述符。
startup.processChannel = function() {
// If we were spawned with env NODE_CHANNEL_FD then load that up and
// start parsing data from that stream.
if (process.env.NODE_CHANNEL_FD) {
var fd = parseInt(process.env.NODE_CHANNEL_FD, 10);
assert(fd >= 0);
// Make sure it's not accidentally inherited by child processes.
delete process.env.NODE_CHANNEL_FD;
var cp = NativeModule.require('child_process');
// Load tcp_wrap to avoid situation where we might immediately receive
// a message.
// FIXME is this really necessary?
process.binding('tcp_wrap');
cp._forkChild(fd);
assert(process.send);
}
};
子进程在启动的过程中连接IPC的FD
exports._forkChild = function(fd) {
// set process.send()
var p = new Pipe(true);
p.open(fd);
p.unref();
const control = setupChannel(process, p);
process.on('newListener', function(name) {
if (name === 'message' || name === 'disconnect') control.ref();
});
process.on('removeListener', function(name) {
if (name === 'message' || name === 'disconnect') control.unref();
});
};
建立连接后父子进程就可以自由的,全双工的通信了。
句柄传递
ChildProcess 类的实例,通过调用 ChildProcess#send(message[, sendHandle[, options]][, callback]) 方法,我们可以实现与子进程的通信,其中的 sendHandle 参数支持传递 net.Server ,net.Socket 等多种句柄,使用它,我们可以很轻松的实现在进程间转发 TCP socket。
send方法可以发送的对象包括如下集中:
- net.Socket对象: TCP套接字
- net.Server对象: TCP服务器
- net.Native: C++层面的TCP套接字和IPC管道
- dgram.Socket: UDP套接字
- dgram.Native: C++层面的UDP套接字
传递的过程:
主进程:
- 传递消息和句柄。
- 将消息包装成内部消息,使用 JSON.stringify 序列化为字符串。
- 通过对应的 handleConversion[message.type].send 方法序列化句柄。
- 将序列化后的字符串和句柄发入 IPC channel 。
子进程:
- 使用 JSON.parse 反序列化消息字符串为消息对象。
- 触发内部消息事件(internalMessage)监听器。
- 将传递来的句柄使用 handleConversion[message.type].got 方法反序列化为 JavaScript 对象。
- 带着消息对象中的具体消息内容和反序列化后的句柄对象,触发用户级别事件。
总结
很多应用比如 redis提供了本地访问的接口,进程通信使用的是 socket 的回环地址。当然它是通用性的考虑,否则要区分本地环境还是网络环境,如果不考虑这点,其实可以用 unix domain socket 代替,以获取更好的相互性能。
Here you have the results on a single CPU 3.3GHz Linux machine :
类型 | TCP | UDS | PIPE |
---|---|---|---|
latency | 6us | 2us | 2us |
throughput | 253702 msg/s | 1733874 msg/s | 1682796 msg/s |
- UDS: UNIX Domain Socket