node進程間通訊

作為一名合格的程式猿/媛,對於進程、執行緒還是有必要了解一點的,本文將從下面幾個方向進行梳理,盡量做到知其然並知其所以然:

  • 進程和執行緒的概念和關係
  • 進程演進
  • 進程間通訊
  • 理解底層基礎,助力上層應用
  • 進程保護

進程和執行緒的概念和關係

用戶下達運行程式的命令後,就會產生進程。同一程式可產生多個進程(一對多關係),以允許同時有多位用戶運行同一程式,卻不會相衝突。

進程需要一些資源才能完成工作,如CPU使用時間、存儲器、文件以及I/O設備,且為依序逐一進行,也就是每個CPU核心任何時間內僅能運行一項進程。

進程與執行緒的區別:進程是電腦管理運行程式的一種方式,一個進程下可包含一個或者多個執行緒。執行緒可以理解為子進程。

摘自wiki百科

也就是說,進程是我們運行的程式程式碼和佔用的資源總和,執行緒是進程的最小執行單位,當然也支援並發。可以說是把問題細化,分成一個個更小的問題,進而得以解決。

並且進程內的執行緒是共享進程資源的,處於同一地址空間,所以切換和通訊相對成本小,而進程可以理解為沒有公共的包裹容器

但是如果進程間需要通訊的話,也需要一個公共環境或者一個媒介,這個就是作業系統。

進程演進

我們的電腦有單核的、多核的,也有多種的組合方式:

  1. 單進程

因為是一個進程,所以某一時刻只能處理一個事務,後續需要等待,體驗不好

  1. 多進程

為了解決上面的問題,但是如果有很多請求的話,會產生很多進程,開銷本身就是一個不小的問題,而進程佔據獨立的記憶體,這麼多響應是的進程難免會有重複的狀態和數據,會造成資源浪費。

  1. 多進程多執行緒

由之前的進程處理事務,改成使用執行緒處理事務,解決了開銷大,資源浪費的問題,還可以使用執行緒池,預先創建就緒執行緒,減少創建和銷毀執行緒的開銷。

但是一個cpu某一時刻只能處理一個事務。像時間分片來調度執行緒的話,會導致執行緒切換頻繁,是非常耗時的。

  1. 單進程單執行緒

類似也就是v8,基於事件驅動,有效的避免了記憶體開銷和上下文切換,只需要執行緒間通訊,即可在適當的時刻進行事務結果等的回饋。

但是遇到計算量很大的事務,會阻塞後續任務的執行。像這樣:

  1. 單進程單執行緒(多進程架構)

node提供了clusterchild_process兩個模組進行進程的創建,也就是我們常說的主(Master)從(Worker)模式。Master負責任務調度和管理Worker進程,Worker進行事務處理。

進程間通訊

node本身提供了cluster和child_process模組創建子進程,本質上cluster.fork()是child_process.fork()的上層實現,cluster帶來的好處是可以監聽共享埠,否則建議使用child_process。

child_process

child_process提供了非同步和同步的操作方法,具體可查看文檔

常見的非同步方法有:

  1. .exec
  2. .execFile
  3. .fork
  4. .spawn

除了fork出來的進程會長期駐存外,其他方式會在子進程任務完成後以流的方式返回並銷毀進程。

非同步方法會返回ChildProcess的實例,ChildProcess不能直接創建,只能返回。

來看幾張圖吧:

舉個例子

有一個很長很長的循環,如果不開啟子進程,會等循環之後才能執行之後的邏輯。

我們可以將耗時的循環放到子進程中,主進程會接受子進程的返回,不影響後續事物的處理。

// 主進程
const execFile = require('child_process').execFile;

execFile('./child.js', [], (err, stdout, stderr) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log(`stdout: ${stdout}`);
});
console.log('用戶事務處理');

// 子進程
#!/usr/bin/env node

for (let i = 0; i < 10000; i++) {
    process.stdout.write(`${i}`);
}

而對於fork,它是專門用來生產子進程的,也可以說是主進程的拷貝,返回的ChildProcess中會內置額外的通訊通道,也就是IPC通道,允許消息在父子進程間傳遞,例如通過文件描述符,不過由於創建的是匿名通道,所以只有主進程可以與之通訊,其他進程無法進行通訊。但相對的還有命名通道,詳見下一節。

看一個簡單的例子:

//parent.js
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
n.on('message', (m) => {
    console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });

//sub.js
process.on('message', (m) => {
    console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });

父進程通過fork返回的ChildProcess進行通訊的監聽和發送,子進程通過全局變數process進行監聽和發送。

cluster

cluster本質上也是通過child_process.fork創建子進程,他還能幫我們合理的管理進程。

const cluster = require('cluster');
// 判斷是否為主進程
if (cluster.isMaster) {
    const cpuNum = require('os').cpus().length;
    for (let i = 0; i < cpuNum; ++i) {
        cluster.fork();
    }

    cluster.on('online', (worker) => {
        console.log('Create worker-' + worker.process.pid);
    });

    cluster.on('exit', (worker, code, signal) => {
        console.log(
            '[Master] worker ' +
                worker.process.pid +
                ' died with code:' +
                code +
                ', and' +
                signal
        );
        cluster.fork(); // 重啟子進程
    });
} else {
    const net = require('net');
    net.createServer()
        .on('connection', (socket) => {
            setTimeout(() => {
                socket.end('Request handled by worker-' + process.pid);
            }, 10);
        })
        .listen(8989);
}

細心地你可能發現多個子進程監聽了同一個埠,這樣不會EADDRIUNS嗎?

其實不然,真正監聽埠的是主進程,當前端請求到達時,會將句柄發送給某個子進程。

理解底層基礎,助力上層應用

進程間通訊(IPC)大概有這幾種:

  • 匿名管道
  • 命名管道
  • 訊號量
  • 消息隊列
  • 訊號
  • 共享記憶體
  • 套接字

從技術上劃分又可以劃分成以下四種:

  1. 消息傳遞(管道,FIFO,消息隊列)
  2. 同步(互斥量,條件變數,讀寫鎖等)
  3. 共享記憶體(匿名的,命名的)
  4. 遠程過程調用

文件描述符是什麼?

在linux中一切皆文件,linux會給每個文件分配一個id,這個id就是文件描述符,指針也是文件描述符的一種。這個很好理解,不過我們可以再往深了說,一個進程啟動後,會在內核空間(虛擬空間的一部分)創建一個PCB控制塊,PCB內部有一個文件描述符表,記錄著當前進程所有可用的文件描述符(即當前進程所有打開的文件)。系統出了維護文件描述符表外,還需要維護打開文件表(Open file table)和i-node表(i-node table)。

文件打開表(Open file table)包含文件偏移量,狀態標誌,i-node表指針等資訊

i-node表(i-node table)包括文件類型,文件大小,時間戳,文件鎖等資訊

文件描述符不是一對一的,它可以:

  1. 同一進程的不同文件描述符指向同一文件
  2. 不同進程可以擁有相同的文件描述符(比如fork出的子進程擁有和父進程一樣的文件描述符,或者不同進程打開同一文件)
  3. 不同進程的同一文件描述符也可以指向不同的文件
  4. 不同進程的不同文件描述符也可以指向同一個文件

上面提及了很多可以實現進程間通訊的方式,那node進程間通訊是以什麼為基礎的呢?

nodeIPC通過管道技術 加 事件循環方式進行通訊,管道技術在windows下由命名管道實現,在*nix系統則由Unix Domain socket實現,提供給我們的是簡單的message事件和send方法。

那管道是什麼呢?

管道實際上是在內核中開闢一塊緩衝區,它有一個讀端一個寫端,並傳給用戶程式兩個文件描述符,一個指向讀端,一個指向寫埠,然後該快取區存儲不同進程間寫入的內容,並供不同進程讀取內容,進而達到通訊的目的。

管道又分為匿名管道和命名管道,匿名管道常見於一個進程fork出子進程,只能親緣進程通訊,而命名管道可以讓非親緣進程進行通訊。

其實本質上來說進程間通訊是利用內核管理一塊記憶體,不同進程可以讀寫這塊內容,進而可以互相通訊,當然,說起來簡單,做起來難。有興趣的朋友可以自行研究。

進程保護

可以用cluster建立主從進程架構,主進程調度管理和分發任務給子進程,並在子進程掛掉或斷開連接後重啟。

pm2是對cluster的一種封裝,提供了:

  • 內奸負載均衡
  • 後台運行
  • 停機重載
  • 具有Ubuntu、CentOS的啟動腳本
  • 停止不穩定的進程
  • 控制台檢測
  • 有好的可視化介面

具體原理和細節以後有空再做分析。

文中若有錯誤的地方,歡迎指出,我會及時更新。希望讀者借鑒的閱讀。

部分圖片來源網路,侵權立刪

參考鏈接

進程、執行緒、協程

文件描述符

IPC

IPC2

Tags: