# 進程
* [`[Doc]` Process (進程)](sections/process.md#process)
* [`[Doc]` Child Processes (子進程)](sections/process.md#child-process)
* [`[Doc]` Cluster (集群)](sections/process.md#cluster)
* [`[Basic]` 進程間通信](sections/process.md#進程間通信)
* [`[Basic]` 守護進程](sections/process.md#守護進程)
## 簡述
關于 Process, 我們需要討論的是兩個概念, ①操作系統的進程, ② Node.js 中的 Process 對象. 操作進程對于服務端而言, 好比 html 之于前端一樣基礎. 想做服務端編程是不可能繞過 Unix/Linux 的. 在 Linux/Unix/Mac 系統中運行 `ps -ef` 命令可以看到當前系統中運行的進程. 各個參數如下:
|列名稱|意義|
|-----|---|
|UID|執行該進程的用戶ID|
|PID|進程編號|
|PPID|該進程的父進程編號|
|C|該進程所在的CPU利用率|
|STIME|進程執行時間|
|TTY|進程相關的終端類型|
|TIME|進程所占用的CPU時間|
|CMD|創建該進程的指令|
關于進程以及操作系統一些更深入的細節推薦閱讀 APUE, 即《Unix 高級編程》等書籍來了解.
## Process
這里來討論 Node.js 中的 `process` 對象. 直接在代碼中通過 `console.log(process)` 即可打印出來. 可以看到 process 對象暴露了非常多有用的屬性以及方法, 具體的細節見[官方文檔](https://nodejs.org/dist/latest-v6.x/docs/api/process.html), 已經說的挺詳細了. 其中包括但不限于:
* 進程基礎信息
* 進程 Usage
* 進程級事件
* 依賴模塊/版本信息
* OS 基礎信息
* 賬戶信息
* 信號收發
* 三個標準流
### process.nextTick
上一節已經提到過 `process.nextTick` 了, 這是一個你需要了解的, 重要的, 基礎方法.
```
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
```
`process.nextTick` 并不屬于 Event loop 中的某一個階段, 而是在 Event loop 的每一個階段結束后, 直接執行 `nextTickQueue` 中插入的 "Tick", 并且直到整個 Queue 處理完. 所以面試時又有可以問的問題了, 遞歸調用 process.nextTick 會怎么樣? (doge
```javascript
function test() {
process.nextTick(() => test());
}
```
這種情況與以下情況, 有什么區別? 為什么?
```javascript
function test() {
setTimeout(() => test(), 0);
}
```
### 配置
配置是開發部署中一個很常見的問題. 普通的配置有兩種方式, 一是定義配置文件, 二是使用環境變量.

你可以通過[設置環境變量](http://cn.bing.com/search?q=linux+%E8%AE%BE%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F)來指定配置, 然后通過 `process.env` 來獲取配置項. 另外也可以通過讀取定義好的配置文件來獲取, 在這方面有很多不錯的庫例如 `dotenv`, `node-config` 等, 而在使用這些庫來加載配置文件的時候, 通常都會碰到一個當前工作目錄的問題.
> <a name="q-cwd"></a> 進程的當前工作目錄是什么? 有什么作用?
當前進程啟動的目錄, 通過 process.cwd() 獲取當前工作目錄 (current working directory), 通常是命令行啟動的時候所在的目錄 (也可以在啟動時指定), 文件操作等使用相對路徑的時候會相對當前工作目錄來獲取文件.
一些獲取配置的第三方模塊就是通過你的當前目錄來找配置文件的. 所以如果你錯誤的目錄啟動腳本, 可能沒法得到正確的結果. 在程序中可以通過 `process.chdir()` 來改變當前的工作目錄.
### 標準流
在 process 對象上還暴露了 `process.stderr`, `process.stdout` 以及 `process.stdin` 三個標準流, 熟悉 C/C++/Java 的同學應該對此比較熟悉. 關于這幾個流, 常見的面試問題是問 **console.log 是同步還是異步? 如何實現一個 console.log?**
如果簡歷中有出現 C/C++ 關鍵字, 一般都會問到如何實現一個同步的輸入 (類似實現C語言的 `scanf`, C++ 的 `cin`, Python 的 `raw_input` 等).
### 維護方面
熟悉與進程有關的基礎命令, 如 top, ps, pstree 等命令.
## Child Process
子進程 (Child Process) 是進程中一個重要的概念. 你可以通過 Node.js 的 `child_process` 模塊來執行可執行文件, 調用命令行命令, 比如其他語言的程序等. 也可以通過該模塊來將 .js 代碼以子進程的方式啟動. 比較有名的網易的分布式架構 [pomelo](https://github.com/NetEase/pomelo) 就是基于該模塊 (而不是 `cluster`) 來實現多進程分布式架構的.
> <a name="q-fork"></a> child_process.fork 與 POSIX 的 fork 有什么區別?
Node.js 的 `child_process.fork()` 不像 POSIX [fork(2)](http://man7.org/linux/man-pages/man2/fork.2.html) 系統調用, 不會拷貝當前父進程. 這里對于其他語言轉過的同學可能比較誤導, 可以作為一個比較偏的面試題.
* spawn() 啟動一個子進程來執行命令
* options.detached 父進程死后是否允許子進程存活
* options.stdio 指定子進程的三個標準流
* spawnSync() 同步版的 spawn, 可指定超時, 返回的對象可獲得子進程的情況
* exec() 啟動一個子進程來執行命令, 帶回調參數獲知子進程的情況, 可指定進程運行的超時時間
* execSync() 同步版的 exec(), 可指定超時, 返回子進程的輸出 (stdout)
* execFile() 啟動一個子進程來執行一個可執行文件, 可指定進程運行的超時時間
* execFileSync() 同步版的 execFile(), 返回子進程的輸出, 如何超時或者 exit code 不為 0, 會直接 throw Error
* fork() 加強版的 spawn(), 返回值是 ChildProcess 對象可以與子進程交互
其中 exec/execSync 方法會直接調用 bash 來解釋命令, 所以如果有命令有外部參數, 則需要注意被注入的情況.
### child.kill 與 child.send
常見會問的面試題, 如 `child.kill` 與 `child.send` 的區別. 二者一個是基于信號系統, 一個是基于 IPC.
> <a name="q-child"></a> 父進程或子進程的死亡是否會影響對方? 什么是孤兒進程?
子進程死亡不會影響父進程, 不過子進程死亡時(線程組的最后一個線程,通常是“領頭”線程死亡時),會向它的父進程發送死亡信號. 反之父進程死亡, 一般情況下子進程也會隨之死亡, 但如果此時子進程處于可運行態、僵死狀態等等的話, 子進程將被`進程1`(init 進程)收養,從而成為孤兒進程. 另外, 子進程死亡的時候(處于“終止狀態”),父進程沒有及時調用 `wait()` 或 `waitpid()` 來返回死亡進程的相關信息,此時子進程還有一個 `PCB` 殘留在進程表中,被稱作僵尸進程.
## Cluster
Cluster 是常見的 Node.js 利用多核的辦法. 它是基于 `child_process.fork()` 實現的, 所以 cluster 產生的進程之間是通過 IPC 來通信的, 并且它也沒有拷貝父進程的空間, 而是通過加入 cluster.isMaster 這個標識, 來區分父進程以及子進程, 達到類似 POSIX 的 [fork](http://man7.org/linux/man-pages/man2/fork.2.html) 的效果.
```javascript
const cluster = require('cluster'); // | |
const http = require('http'); // | |
const numCPUs = require('os').cpus().length; // | | 都執行了
// | |
if (cluster.isMaster) { // |-|-----------------
// Fork workers. // |
for (var i = 0; i < numCPUs; i++) { // |
cluster.fork(); // |
} // | 僅父進程執行 (a.js)
cluster.on('exit', (worker) => { // |
console.log(`${worker.process.pid} died`); // |
}); // |
} else { // |-------------------
// Workers can share any TCP connection // |
// In this case it is an HTTP server // |
http.createServer((req, res) => { // |
res.writeHead(200); // | 僅子進程執行 (b.js)
res.end('hello world\n'); // |
}).listen(8000); // |
} // |-------------------
// | |
console.log('hello'); // | | 都執行了
```
在上述代碼中 numCPUs 雖然是全局變量但是, 在父進程中修改它, 子進程中并不會改變, 因為父進程與子進程是完全獨立的兩個空間. 他們所謂的共有僅僅只是都執行了, 并不是同一份.
你可以把父進程執行的部分當做 `a.js`, 子進程執行的部分當做 `b.js`, 你可以把他們想象成是先執行了 `node a.js` 然后 cluster.fork 了幾次, 就執行執行了幾次 `node b.js`. 而 cluster 模塊則是二者之間的一個橋梁, 你可以通過 cluster 提供的方法, 讓其二者之間進行溝通交流.
### How It Works
worker 進程是由 child_process.fork() 方法創建的, 所以可以通過 IPC 在主進程和子進程之間相互傳遞服務器句柄.
cluster 模塊提供了兩種分發連接的方式.
第一種方式 (默認方式, 不適用于 windows), 通過時間片輪轉法(round-robin)分發連接. 主進程監聽端口, 接收到新連接之后, 通過時間片輪轉法來決定將接收到的客戶端的 socket 句柄傳遞給指定的 worker 處理. 至于每個連接由哪個 worker 來處理, 完全由內置的循環算法決定.
第二種方式是由主進程創建 socket 監聽端口后, 將 socket 句柄直接分發給相應的 worker, 然后當連接進來時, 就直接由相應的 worker 來接收連接并處理.
使用第二種方式時, 多個 worker 之間會存在競爭關系, 產生一個老生常談的 "[驚群效應](https://www.google.com.hk/search?q=%E6%83%8A%E7%BE%A4%E6%95%88%E5%BA%94)" 從而導致效率變低的問題. 該問題常見于 Apache. 并且各自競爭的情況下無法控制一個新的連接由哪個進程來處理, 從而導致各 worker 進程之間的負載不均衡, 比如通常 70% 的連接僅被 8 個進程中的 2 個處理, 而其他進程比較清閑.
## 進程間通信
IPC (Inter-process communication) 進程間通信技術. 常見的進程間通信技術列表如下:
類型|無連接|可靠|流控制|優先級
---|-----|----|-----|-----
普通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
Node.js 中的 IPC 通信是由 libuv 通過管道技術實現的, 在 windows 下由命名管道(named pipe)實現也就是上表中的最后第二個, *nix 系統則采用 UDS (Unix Domain Socket) 實現.
普通的 socket 是為網絡通訊設計的, 而網絡本身是不可靠的, 而為 IPC 設計的 socket 則不然, 因為默認本地的網絡環境是可靠的, 所以可以簡化大量不必要的 encode/decode 以及計算校驗等, 得到效率更高的 UDS 通信.
如果了解 Node.js 的 IPC 的話, 可以問個比較有意思的問題
> <a name="q-ipc-fd"></a> 在 IPC 通道建立之前, 父進程與子進程是怎么通信的? 如果沒有通信, 那 IPC 是怎么建立的?
這個問題也挺簡單, 只是個思路的問題. 在通過 child_process 建立子進程的時候, 是可以指定子進程的 env (環境變量) 的. 所以 Node.js 在啟動子進程的時候, 主進程先建立 IPC 頻道, 然后將 IPC 頻道的 fd (文件描述符) 通過環境變量 (`NODE_CHANNEL_FD`) 的方式傳遞給子進程, 然后子進程通過 fd 連上 IPC 與父進程建立連接.
最后于進程間通信 (IPC) 的問題, 一般不會直接問 IPC 的實現, 而是會問什么情況下需要 IPC, 以及使用 IPC 處理過什么業務場景等.
## 守護進程
最后的守護進程, 是服務端方面一個很基礎的概念了. 很多人可能只知道通過 pm2 之類的工具可以將進程以守護進程的方式啟動, 卻不了解什么是守護進程, 為什么要用守護進程. 對于水平好的同學, 我們是希望能了解守護進程的實現的.
普通的進程, 在用戶退出終端之后就會直接關閉. 通過 `&` 啟動到后臺的進程, 之后會由于會話(session組)被回收而終止進程. 守護進程是不依賴終端(tty)的進程, 不會因為用戶退出終端而停止運行的進程.
```c
// 守護進程實現 (C語言版本)
void init_daemon()
{
pid_t pid;
int i = 0;
if ((pid = fork()) == -1) {
printf("Fork error !\n");
exit(1);
}
if (pid != 0) {
exit(0); // 父進程退出
}
setsid(); // 子進程開啟新會話, 并成為會話首進程和組長進程
if ((pid = fork()) == -1) {
printf("Fork error !\n");
exit(-1);
}
if (pid != 0) {
exit(0); // 結束第一子進程, 第二子進程不再是會話首進程
// 避免當前會話組重新與tty連接
}
chdir("/tmp"); // 改變工作目錄
umask(0); // 重設文件掩碼
for (; i < getdtablesize(); ++i) {
close(i); // 關閉打開的文件描述符
}
return;
}
```
[Node.js 編寫守護進程](https://cnodejs.org/topic/57adfadf476898b472247eac)