node進程間通訊

做爲一名合格的程序猿/媛,對於進程、線程仍是有必要了解一點的,本文將從下面幾個方向進行梳理,儘可能作到知其然並知其因此然:javascript

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

進程和線程的概念和關係

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

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

進程與線程的區別:進程是計算機管理運行程序的一種方式,一個進程下可包含一個或者多個線程。線程能夠理解爲子進程。java

摘自wiki百科node

也就是說,進程是咱們運行的程序代碼和佔用的資源總和,線程是進程的最小執行單位,固然也支持併發。能夠說是把問題細化,分紅一個個更小的問題,進而得以解決。linux

而且進程內的線程是共享進程資源的,處於同一地址空間,因此切換和通訊相對成本小,而進程能夠理解爲沒有公共的包裹容器git

可是若是進程間須要通訊的話,也須要一個公共環境或者一個媒介,這個就是操做系統。github

進程演進

咱們的計算機有單核的、多核的,也有多種的組合方式:chrome

  1. 單進程

由於是一個進程,因此某一時刻只能處理一個事務,後續須要等待,體驗很差windows

  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

相關文章
相關標籤/搜索