SunShinewyf/issue-blog

node中的process模块

SunShinewyf opened this issue · 12 comments

process对象是一个global(全局变量),它对于Node.js应用程序始终是可用的,所以在使用时无需使用require()

对于process部分的API,这里不详细讲,具体可移步这里

node 中的 console.log

js中的console.lognode中的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.writenode中的console.log是同步还是异步的呢,也就是process.stdout.write是同步的还是异步的。官方文档给出的解释是:

写操作是否为同步,取决于连接的是什么流以及操作系统是 Windows 还是 POSIX :

  • Files: 同步 在 Windows 和 POSIX 下
  • TTYs (Terminals): 异步 在 Windows 下, 同步 在 POSIX 下
  • Pipes (and sockets): 同步 在 Windows 下, 异步 在 POSIX 下

process 的 child_process

child_processnode中一个比较重要的模块,众所周知,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,所以execFileexec更加安全,也更高效。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

clusternode内置的一个模块,用于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源码确实没看过。

@xtx1130 链接 建议看看,还不错~

@SunShinewyf 哈哈,我之前star过一个YDK JS

应该是一样的,只是译本吧,哈哈