node中的process模块
SunShinewyf opened this issue · 12 comments
process
对象是一个global
(全局变量),它对于Node.js
应用程序始终是可用的,所以在使用时无需使用require()
。
对于process
部分的API,这里不详细讲,具体可移步这里
node 中的 console.log
js
中的console.log
和node
中的console.log
还是有一些区别的。
js
中的console.log
在一些情况下执行时存在异步情况,根据《你不知道的JavaScript中卷》中的描述:
并没有什么规范或一组需求指定console.* 方法族如何工作——它们并不是JavaScript 正式
的一部分,而是由宿主环境(请参考本书的“类型和语法”部分)添加到JavaScript 中的。因此,不同的浏览器和JavaScript 环境可以按照自己的意愿来实现,有时候这会引起混淆。
尤其要提出的是,在某些条件下,某些浏览器的console.log(..) 并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是JavaScript)中,I/O 是非常低速的阻塞部分。所以,(从页面/UI 的角度来说)浏览器在后台异步处理控制台I/O 能够提高性能,这时用户甚至可能根本意识不到其发生。
下面这种情景不是很常见,但也可能发生,从中(不是从代码本身而是从外部)可以观察到这种情况:
PS:可以试试这个
var a = {
index: 1
};
// 然后
console.log( a ); // ??
// 再然后
a.index++;
我们通常认为恰好在执行到console.log(..) 语句的时候会看到a 对象的快照,打印出类
似于{ index: 1 } 这样的内容,然后在下一条语句a.index++ 执行时将其修改,这句的执
行会严格在a 的输出之后。
多数情况下,前述代码在开发者工具的控制台中输出的对象表示与期望是一致的。
但是,这段代码运行的时候,浏览器可能会认为需要把控制台I/O 延迟到后台,在这种情况下,等到浏览器控制台输出对象内容时,a.index++ 可能已经执行,因此会显示{ index: 2 }。
到底什么时候控制台I/O 会延迟,甚至是否能够被观察到,这都是游移不定的。
如果在调试的过程中遇到对象在console.log(..) 语句之后被修改,可你却看到了意料之外的结果,要意识到这可能是这种I/O 的异步化造成的。
可见,js
中的console.log
并不是严格同步或者异步,而是取决于执行环境和I/O之间的异步化。
在node
中,console.log
的底层实现代码如下:
Console.prototype.log = function log(...args) {
write(this._ignoreErrors,
this._stdout,
util.format.apply(null, args),
this._stdoutErrorHandler,
this[kGroupIndent]);
};
底层使用的是process.stdout.write
。node
中的console.log
是同步还是异步的呢,也就是process.stdout.write
是同步的还是异步的。官方文档给出的解释是:
写操作是否为同步,取决于连接的是什么流以及操作系统是 Windows 还是 POSIX :
- Files: 同步 在 Windows 和 POSIX 下
- TTYs (Terminals): 异步 在 Windows 下, 同步 在 POSIX 下
- Pipes (and sockets): 同步 在 Windows 下, 异步 在 POSIX 下
process 的 child_process
child_process
是node
中一个比较重要的模块,众所周知,node有一个一直被人诟病的地方就是“单进程单线程”,但是有了child_process
之后,node就可以实现在程序中直接创建子进程,除此之外,子进程和主进程之间还可以进行通信,这样就榨干了cpu的资源,使资源得到了充分地利用。
如何创建一个子进程,创建同步进程:
- child_process.execFileSync(file[, args][, options])
- child_process.execSync(command[, options])
- child_process.spawnSync(command[, args][, options])
创建异步进程:
- child_process.exec(command[, options][, callback])
- child_process.execFile(file[, args][, options][, callback])
- child_process.fork(modulePath[, args][, options])
- child_process.spawn(command[, args][, options])
各个不同的方法之间的关系如下:
exec、execFile、fork
都是通过spawn
封装而成,由此可见,spawn
是最基础的,它只能运行指定的程序,参数需要在列表中列出,但是exec
在执行时则衍生出一个shell并在shell上运行。和exec
类似的是execFile
,但它执行命令,无需衍生出一个shell,所以execFile
比exec
更加安全,也更高效。fork
也是在spawn
中封装出来的,专门用于衍生新的Node.js进程,跟 child_process.spawn() 一样返回一个 ChildProcess 对象。 返回的 ChildProcess 会有一个额外的内置的通信通道,它允许消息在父进程和子进程之间来回传递。
详细的可以参见官方文档
孤儿进程和僵尸进程
在unix/linux中,子进程是父进程创建的,子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
- 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。init进程会循环wait()它的退出的子进程并进行善后工作,所以孤儿进程并不会带来什么实质性的危害。
- 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。一旦有很多只处理少量任务的子进程完成任务后就退出,然后父进程又不管子进程的退出,然后就会产生很多的僵死进程,这样会对程序产生一定的危害。
node中 的 cluster
cluster
是node
内置的一个模块,用于node多核处理,它的工作进程由child_process.fork()
方法创建,因此它们可以使用IPC和父进程通信,从而使各进程交替处理连接服务。cluster
中创建worker
的源码如下:
function createWorkerProcess(id, env) {
//省略一些代码
return fork(cluster.settings.exec, cluster.settings.args, {
env: workerEnv,
silent: cluster.settings.silent,
execArgv: execArgv,
stdio: cluster.settings.stdio,
gid: cluster.settings.gid,
uid: cluster.settings.uid
});
}
而且手动传了一些环境变量的参数值。如下是根据cluster.isMaster
标识来fork
子进程的代码:
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
//master和worker之间的通信
cluster.on('fork', function (worker) {
console.log('[master] ' + 'fork: worker' + worker.id);
});
cluster.on('online', function (worker) {
console.log('[master] ' + 'online: worker' + worker.id);
});
cluster.on('listening', function (worker, address) {
console.log('[master] ' + 'listening: worker' + worker.id + ',pid:' + worker.process.pid + ', Address:' + address.address + ":" + address.port);
});
cluster.on('disconnect', function (worker) {
console.log('[master] ' + 'disconnect: worker' + worker.id);
});
cluster.on('exit', function (worker, code, signal) {
console.log('[master] ' + 'exit worker' + worker.id + ' died');
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
}
console.log('hello');
cluster
模块支持两种连接分发模式(将新连接安排给某一工作进程处理)。
第一种方法(也是除Windows
外所有平台的默认方法),是循环法。由主进程负责监听端口,接收新连接后再将连接循环分发给工作进程。在分发中使用了一些内置技巧防止工作进程任务过载。
第二种方法是,主进程创建监听socket后发送给感兴趣的工作进程,由工作进程负责直接接收连接。
理论上第二种方法应该是效率最佳的,但在实际情况下,由于操作系统调度机制的难以捉摸,会使分发变得不稳定。我们遇到过这种情况:8个进程中的2个,分担了70%的负载。
总结
这里只是集合一些知识点,结合饿了么前端面试整理了一些自己对这一块不太清楚的知识点,权当笔记用了,以后可以方便回顾~
参考文档:
感觉博主说的console这块有点问题啊,console是基于process.stdout.write,但是console.log(a),a这个参数是直接传进来的:代码链接。
这这里是对args的处理,再看write:代码链接,在调用process.stdout.write之前已经拼接好string了,所以应该是console.log的args是同步的,只不过在输出的时候调用的process.stdout.write。
而write的异步问题,确实代码里面进行了兼容处理了:代码链接,通过event emitter的once方法对error进行监听,这样就相当于把write丢到了libuv的event loop中了,无论同步异步,只要触发了error,就会打断,而下面的catch,则是怕运行栈溢出,直接hold住uv_defalut_loop了,所以会catch一下,做下溢出判断。
但是wirte里面只是调用了stream.write(string, errorhandler);而且try里面只是捕获同步错误,对于你上面说的不论同步还是异步,都会走进catch有点不太理解,而且这个容错处理和stream.write是同步还是异步有什么关系呢
异步不会走到catch,我这里说的确实有问题。
你关联一下上下文,stream.write其实是process.stdout.write,而process.stdout官网api文档介绍了是一个socket流,因此会继承于event emitter,所以
stream.once('error', noop);
这个方法是存在的,而error事件是error handler,专门用来监听error的。
如果process.stdout是异步的,那么可以直接监听到process.stderr,然后通过
stream.write(string, errorhandler);
errorhandler来返回错误信息。
如果process.stdout是同步的且在执行的时候存在运行栈溢出的情况,这种时候通过监听'error'事件可以判断是否是运行栈溢出,因为stream.once
是event emitter,所以回调noop会等到主loop执行完后执行。如果运行栈溢出,noop无法被触发,会抛错并走到catch流程中,而在catch中,如果是第一次运行栈溢出,会通过try来拿到运行栈溢出错误的message:
if (MAX_STACK_MESSAGE === undefined) {
try {
// eslint-disable-next-line no-unused-vars
function a() { a(); }
} catch (err) {
MAX_STACK_MESSAGE = err.message;
}
}
之后,通过对比第一次try的e.message和运行栈溢出的错误信息来抛出错误:
if (e.message === MAX_STACK_MESSAGE && e.name === 'RangeError')
throw e;
你说的对于write函数中对于错误处理这块我是很认同的,但对于错误捕获的部分和process.stdout.write是异步还是同步好像没有什么很直接的关联吧,还是说我没有完全get到你想要表达“我阐述的console这块有问题的点”....
……
但是,这段代码运行的时候,浏览器可能会认为需要把控制台I/O 延迟到后台,在这种情况下,等到浏览器控制台输出对象内容时,a.index++ 可能已经执行,因此会显示{ index: 2 }。
你说的这句话,应该是不会发生的,因为在执行这个函数console.log(a)
的时候,a就已经固定了,引用之前我说过的那句话:
在调用process.stdout.write之前已经拼接好string了
只不过在输出到控制台的时候可能会在a.index++后面,因为输出是用的process.stdout.write。
上面评论中你引用的那个描述:
但是,这段代码运行的时候,浏览器可能会认为需要把控制台I/O 延迟到后台,在这种情况下,等到浏览器控制台输出对象内容时,a.index++ 可能已经执行,因此会显示{ index: 2 }。
是针对js中的console.log来说的,并不是针对node中的console.log,只有node中的console.log才是用的process.stdout.write来进行输出
额,好吧,我读下来感觉你在说node。😓但是我在浏览器中也没遇到过这种情况。不过process.stdin/out底层调用的都是libuv,而chrome底层是开发人员二次开发的libuv2,理论上chrome的表现应该和node差不多,不过我也不十分确定。。。
@xtx1130 chrome中console.log的解释是直接摘自《你不知道的JavaScript》的。不过很感谢和你交流那么多,感觉你研究得很深~
@SunShinewyf 哦哦,YDK js 那本书是么,我还真没仔细看过。。。有时间摘出来翻翻。不过chrome源码确实没看过。
@SunShinewyf 哈哈,我之前star过一个YDK JS
应该是一样的,只是译本吧,哈哈