分享 10 道 Nodejs 进程相关面试题

通过对以下 10 个面试题的分享,助您更好的理解 Node.js 的进程和线程相关知识

快速导航

  • 什么是进程和线程?之间的区别?参考:Interview1
  • 什么是孤儿进程?参考:Interview2
  • 创建多进程时,代码里有 app.listen(port) 在进行 fork 时,为什么没有报端口被占用?参考:Interview3
  • 什么是 IPC 通信,如何建立 IPC 通信?什么场景下需要用到 IPC 通信?参考:Interview4
  • Node.js 是单线程还是多线程?进一步会提问为什么是单线程?参考:Interview5
  • 关于守护进程,是什么、为什么、怎么编写?参考:Interview6
  • 实现一个简单的命令行交互程序?参考:Interview7
  • 如何让一个 js 文件在 Linux 下成为一个可执行命令程序?参考:Interview8
  • 进程的当前工作目录是什么? 有什么作用?参考:Interview9
  • 多进程或多个 Web 服务之间的状态共享问题?参考:Interview10

作者简介:五月君,Nodejs Developer,热爱技术、喜欢分享的 90 后青年,公众号 “Nodejs技术栈”,Github 开源项目 https://www.nodejs.red

Interview1

什么是进程和线程?之间的区别?

关于线程和进程是服务端一个很基础的概念,在文章 Node.js进阶之进程与线程 中介绍了进程与线程的概念之后又给出了在 Node.js 中的进程和线程的实际应用,对于这块不是很理解的建议先看下。

Interview2

什么是孤儿进程?

父进程创建子进程之后,父进程退出了,但是父进程对应的一个或多个子进程还在运行,这些子进程会被系统的 init 进程收养,对应的进程 ppid 为 1,这就是孤儿进程。通过以下代码示例说明。

// master.js
const fork = require('child_process').fork;
const server = require('net').createServer();
server.listen(3000);
const worker = fork('worker.js');

worker.send('server', server);
console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
process.exit(0); // 创建子进程之后,主进程退出,此时创建的 worker 进程会成为孤儿进程
// worker.js
const http = require('http');
const server = http.createServer((req, res) => {
    res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); // 记录当前工作进程 pid 及父进程 ppid
});

let worker;
process.on('message', function (message, sendHandle) {
    if (message === 'server') {
        worker = sendHandle;
        worker.on('connection', function(socket) {
            server.emit('connection', socket);
        });
    }
});

孤儿进程 示例源码

控制台进行测试,输出当前工作进程 pid 和 父进程 ppid

$ node master
worker process created, pid: 32971 ppid: 32970

由于在 master.js 里退出了父进程,活动监视器所显示的也就只有工作进程。

[图片上传失败...(image-1cfbab-1560814309268)]

再次验证,打开控制台调用接口,可以看到工作进程 32971 对应的 ppid 为 1(为 init 进程),此时已经成为了孤儿进程

$ curl http://127.0.0.1:3000
I am worker, pid: 32971, ppid: 1

Interview3

创建多进程时,代码里有 app.listen(port) 在进行 fork 时,为什么没有报端口被占用?

先看下端口被占用的情况

// master.js
const fork = require('child_process').fork;
const cpus = require('os').cpus();

for (let i=0; i<cpus.length; i++) {
    const worker = fork('worker.js');
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
}
//worker.js
const http = require('http');
http.createServer((req, res) => {
    res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
}).listen(3000);

多进程端口占用冲突 示例源码

以上代码示例,控制台执行 node master.js 只有一个 worker 可以监听到 3000 端口,其余将会抛出 Error: listen EADDRINUSE :::3000 错误

那么多进程模式下怎么实现多端口监听呢?答案还是有的,通过句柄传递 Node.js v0.5.9 版本之后支持进程间可发送句柄功能,怎么发送?如下所示:

/**
 * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
 * message
 * sendHandle
 */
subprocess.send(message, sendHandle)

当父子进程之间建立 IPC 通道之后,通过子进程对象的 send 方法发送消息,第二个参数 sendHandle 就是句柄,可以是 TCP套接字、TCP服务器、UDP套接字等,为了解决上面多进程端口占用问题,我们将主进程的 socket 传递到子进程,修改代码,如下所示:

//master.js
const fork = require('child_process').fork;
const cpus = require('os').cpus();
const server = require('net').createServer();
server.listen(3000);
process.title = 'node-master'

for (let i=0; i<cpus.length; i++) {
    const worker = fork('worker.js');
    worker.send('server', server);
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
}
// worker.js
const http = require('http');
http.createServer((req, res) => {
    res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
})

let worker;
process.title = 'node-worker'
process.on('message', function (message, sendHandle) {
    if (message === 'server') {
        worker = sendHandle;
        worker.on('connection', function(socket) {
            server.emit('connection', socket);
        });
    }
});

句柄传递解决多进程端口占用冲突问题 示例源码

验证一番,控制台执行 node master.js 以下结果是我们预期的,多进程端口占用问题已经被解决了。

$ node master.js
worker process created, pid: 34512 ppid: 34511
worker process created, pid: 34513 ppid: 34511
worker process created, pid: 34514 ppid: 34511
worker process created, pid: 34515 ppid: 34511

关于多进程端口占用问题,cnode 上有篇文章也可以看下 通过源码解析 Node.js 中 cluster 模块的主要功能实现

Interview4

什么是 IPC 通信,如何建立 IPC 通信?什么场景下需要用到 IPC 通信?

IPC (Inter-process communication) ,即进程间通信技术,由于每个进程创建之后都有自己的独立地址空间,实现 IPC 的目的就是为了进程之间资源共享访问,实现 IPC 的方式有多种:管道、消息队列、信号量、Domain Socket,Node.js 通过 pipe 来实现。

看一下 Demo,未使用 IPC 的情况

// pipe.js
const spawn = require('child_process').spawn;
const child = spawn('node', ['worker.js'])
console.log(process.pid, child.pid); // 主进程id3243 子进程3244
// worker.js
console.log('I am worker, PID: ', process.pid);

控制台执行 node pipe.js,输出主进程id、子进程id,但是子进程 worker.js 的信息并没有在控制台打印,原因是新创建的子进程有自己的stdio 流。

$ node pipe.js
41948 41949

创建一个父进程和子进程之间传递消息的 IPC 通道实现输出信息

修改 pipe.js 让子进程的 stdio 和当前进程的 stdio 之间建立管道链接,还可以通过 spawn() 方法的 stdio 选项建立 IPC 机制,参考 options.stdio

// pipe.js
const spawn = require('child_process').spawn;
const child = spawn('node', ['worker.js'])
child.stdout.pipe(process.stdout);
console.log(process.pid, child.pid);

父子进程 IPC 通信 源码示例

再次验证,控制台执行 node pipe.js,worker.js 的信息也打印了出来

$ 42473 42474
I am worker, PID:  42474

关于父进程与子进程是如何通信的?

参考了深入浅出 Node.js 一书,父进程在创建子进程之前会先去创建 IPC 通道并一直监听该通道,之后开始创建子进程并通过环境变量(NODE_CHANNEL_FD)的方式将 IPC 频道的文件描述符传递给子进程,子进程启动时根据传递的文件描述符去链接 IPC 通道,从而建立父子进程之间的通信机制。

[图片上传失败...(image-ed7fa4-1560814309268)]

<p style="text-align:center; padding: 10px;">父子进程 IPC 通信交互图</p>

Interview5

Node.js 是单线程还是多线程?进一步会提问为什么是单线程?

第一个问题,Node.js 是单线程还是多线程?这个问题是个基本的问题,在以往面试中偶尔提到还是有不知道的,Javascript 是单线程的,但是做为其在服务端运行环境的 Node.js 并非是单线程的。

第二个问题,Javascript 为什么是单线程?这个问题需要从浏览器说起,在浏览器环境中对于 DOM 的操作,试想如果多个线程来对同一个 DOM 操作是不是就乱了呢,那也就意味着对于DOM的操作只能是单线程,避免 DOM 渲染冲突。在浏览器环境中 UI 渲染线程和 JS 执行引擎是互斥的,一方在执行时都会导致另一方被挂起,这是由 JS 引擎所决定的。

Interview6

关于守护进程,是什么、为什么、怎么编写?

守护进程运行在后台不受终端的影响,什么意思呢?Node.js 开发的同学们可能熟悉,当我们打开终端执行 node app.js 开启一个服务进程之后,这个终端就会一直被占用,如果关掉终端,服务就会断掉,即前台运行模式。如果采用守护进程进程方式,这个终端我执行 node app.js 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。

创建步骤

  1. 创建子进程
  2. 在子进程中创建新会话(调用系统函数 setsid)
  3. 改变子进程工作目录(如:“/” 或 “/usr/ 等)
  4. 父进程终止

Node.js 编写守护进程 Demo 展示

index.js 文件里的处理逻辑使用 spawn 创建子进程完成了上面的第一步操作。设置 options.detached 为 true 可以使子进程在父进程退出后继续运行(系统层会调用 setsid 方法),参考 options_detached,这是第二步操作。options.cwd 指定当前子进程工作目录若不做设置默认继承当前工作目录,这是第三步操作。运行 daemon.unref() 退出父进程,参考 options.stdio,这是第四步操作。

// index.js
const spawn = require('child_process').spawn;

function startDaemon() {
    const daemon = spawn('node', ['daemon.js'], {
        cwd: '/usr',
        detached : true,
        stdio: 'ignore',
    });

    console.log('守护进程开启 父进程 pid: %s, 守护进程 pid: %s', process.pid, daemon.pid);
    daemon.unref();
}

startDaemon()

daemon.js 文件里处理逻辑开启一个定时器每 10 秒执行一次,使得这个资源不会退出,同时写入日志到子进程当前工作目录下

// /usr/daemon.js
const fs = require('fs');
const { Console } = require('console');

// custom simple logger
const logger = new Console(fs.createWriteStream('./stdout.log'), fs.createWriteStream('./stderr.log'));

setInterval(function() {
    logger.log('daemon pid: ', process.pid, ', ppid: ', process.ppid);
}, 1000 * 10);

守护进程实现 Node.js 版本 源码地址

运行测试

$ node index.js
守护进程开启 父进程 pid: 47608, 守护进程 pid: 47609

打开活动监视器查看,目前只有一个进程 47609,这就是我们需要进行守护的进程

[图片上传失败...(image-7c95d-1560814309268)]

守护进程阅读推荐

守护进程总结

在实际工作中对于守护进程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一个简单的 Demo 对守护进程做了一个说明,在实际工作中对守护进程的健壮性要求还是很高的,例如:进程的异常监听、工作进程管理调度、进程挂掉之后重启等等,这些还需要我们去不断思考。

Interview7

采用子进程 child_process 的 spawn 方法,如下所示:

const spawn = require('child_process').spawn;
const child = spawn('echo', ["简单的命令行交互"]);
child.stdout.pipe(process.stdout); // 将子进程的输出做为当前进程的输入,打印在控制台
$ node execfile
简单的命令行交互

Interview8

如何让一个 js 文件在 Linux 下成为一个可执行命令程序?

  1. 新建 hello.js 文件,头部须加上 #!/usr/bin/env node,表示当前脚本使用 Node.js 进行解析
  2. 赋予文件可执行权限 chmod +x chmod +x /${dir}/hello.js,目录自定义
  3. 在 /usr/local/bin 目录下创建一个软链文件 sudo ln -s /${dir}/hello.js /usr/local/bin/hello,文件名就是我们在终端使用的名字
  4. 终端执行 hello 相当于输入 node hello.js
#!/usr/bin/env node

console.log('hello world!');

终端测试

$ hello
hello world!

Interview9

进程的当前工作目录是什么? 有什么作用?

进程的当前工作目录可以通过 process.cwd() 命令获取,默认为当前启动的目录,如果是创建子进程则继承于父进程的目录,可通过 process.chdir() 命令重置,例如通过 spawn 命令创建的子进程可以指定 cwd 选项设置子进程的工作目录。

有什么作用?例如,通过 fs 读取文件,如果设置为相对路径则相对于当前进程启动的目录进行查找,所以,启动目录设置有误的情况下将无法得到正确的结果。还有一种情况程序里引用第三方模块也是根据当前进程启动的目录来进行查找的。

// 示例
process.chdir('/Users/may/Documents/test/') // 设置当前进程目录

console.log(process.cwd()); // 获取当前进程目录

Interview10

多进程或多个 Web 服务之间的状态共享问题?

多进程模式下各个进程之间是相互独立的,例如用户登陆之后 session 的保存,如果保存在服务进程里,那么如果我有 4 个工作进程,每个进程都要保存一份这是没必要的,假设服务重启了数据也会丢失。多个 Web 服务也是一样的,还会出现我在 A 机器上创建了 Session,当负载均衡分发到 B 机器上之后还需要在创建一份。一般的做法是通过 Redis 或者 数据库来做数据共享。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 前言 通过前边的学习,大家应该已经充分理解了node的单线程只不过是js层面的单线程,是基于V8引擎的单线程,因为...
    白昔月阅读 4,548评论 3 13
  • 本系列出于AWeiLoveAndroid的分享,在此感谢,再结合自身经验查漏补缺,完善答案。以成系统。 Java基...
    济公大将阅读 1,524评论 1 6
  • 很多Node.js初学者都会有这样的疑惑,Node.js到底是单线程的还是多线程的?通过本章的学习,能够让读者较为...
    越努力越幸运_952c阅读 3,635评论 4 36
  • # 模块机制 node采用模块化结构,按照CommonJS规范定义和使用模块,模块与文件是一一对应关系,即加载一个...
    RichRand阅读 2,489评论 0 3
  • NodeJs基于事件驱动的服务模型,采用单线程避免了不必要的内存开销和上下文切换的开销,但是同时也带来了一些问题,...
    拂云枝阅读 2,024评论 1 7