30-06 之應用層的 I/O 優化 - Stream ( 與一些 IPC 知識 )
It 鐵人賽 2019
Lastmod: 2019-12-16

正文開始

Stream 這個東東,基本上在每一個語言你都看的到,而今天我們將要深入的來理解它到底是什麼東西,並且它在一些 I/O 操作上可以幫助我們解決什麼事情。

本篇文章將分為以下幾個章節

  • Stream 是什麼 ? 可以解決什麼事情 ?
  • Stream 在 IPC 通信的原理。
  • 簡單的小範例。

Stream 是什麼 ? 可以解決什麼事情 ?

傳統資料傳輸流程問題

stream 它是一種技術,基本上專門用來傳輸資料用。

咱們先來看看傳統上,咱們如果要從硬碟拿個檔案是如何處理。

基本上流程如下圖 1 所示。

  1. 應用程式發送 system call read 某個檔案給作業系統的內核。(用戶切內核)
  2. 內核看看內核緩衝區有沒有相關資料(也就是所謂的內核記憶體)。
  3. 有,則將內核緩衝區的資料,拷貝到用戶緩衝區(用戶進程記憶體)。
  4. 無,則前往硬碟取得。
  5. 將硬碟資料拷貝至內核緩衝區。
  6. 將內核緩存區資料,拷貝到用戶緩衝區。(內核切成用戶)
  7. 然後就可以使用資料了。

圖 1 : 傳統的資料傳輸流程

而重點在於這裡 :

硬碟資料 copy 到內核緩衝區,接下來再從內核緩衝區 copy 到用戶緩衝區。

然後問題出在於 :

如果資料很大會發生什麼事呢 ?

基本上結果就如下圖 2 所示,有可能在內核緩衝或用戶緩衝,就爆掉了,因為記憶體是整份 copy 過去,如果你硬碟資料有 10 gb,那就代表,要將這 10 gb 的資料 copy 到內核緩存,再 copy 到用戶緩存,這時後用戶進程才能拿到它。

但正常來說,不論是內核緩存或用戶緩存都一定要大小限制,如果直接來個 10gb 資料,那一定會炸掉的。

圖 2 : 傳統資料傳輸的問題

而正也是這個原因,才有 stream 這個技術的誕生。

~ 小提醒 ~ 緩衝區就是指某個記憶體區塊,大部份在資料傳輸的時後,每一個地方的應用、包含內核都會在某個記憶體區塊,開啟一些緩衝區,用來存放等等進來的資料。

Stream 原理

stream 的基本原理如下圖 3 所示,它不是直接一次將 10gb 的資料拷則到內核緩存後,再直接全部拷則到用戶緩存,它是一點一點的慢慢拷則,每當拷貝 100 mb ( 這只是範例大小 ) 到內核時就繼續拷貝到用戶,然後用戶再直接將它拿來使用,而當使用完,用戶就是將記憶體清除掉,就不會有什麼爆炸,或記憶體爆漲的問題。

圖 3 : stream 原理

注意

從上圖原理事實上會發現一個事情。

如果它是每 100 mb 就會拷貝到用戶緩存,然後用戶在將它處理掉,那這樣是不是代表每進行一次這個就會進行一次上下文切換呢 ?

嗯對。

不過這裡簡單說明一下,它的上下文切換不會比進程至進程的上下文切換還耗資源,因為它不用處理下一個進程的東西,這也代表不需要記錄與載入其它進程的資訊,比較圖如下圖 4 所示 。這種切換比較常被稱為『 特權模式切換 』,不過事實上它也的確也換是有進行上下文切換。

圖 4 : 特權模式切換與進程上下文切換比較

Stream 在 IPC 通信的原理

接下來咱們來說說 stream 在 ipc 通信的一些原理,咱們先從 ipc 開始來談談。

IPC (InterProcess Communication)

比較白話的說法為 :

它是作業系統提供 process 與 process 的傳輸資料的方式

linux 基本上有提供以下幾種方式 :

  • 管道 ( pipe )
  • 訊息佇列 ( Message queue )
  • 訊號 ( signal )
  • socket

它們的運作原理基本上就如下圖 5 所示,流程如下 :

  1. 進程 A 將在用戶空間緩衝區的資料拷則到內核緩衝區。
  2. 內核再將資料從內核緩衝區拷則到進程 B 的用戶空間緩衝區。

這就是 IPC 基本的運作原理,這裡就先不探討不同 IPC 方法有啥不同,只要先知道原理相同就好。

圖 5 : ipc 的傳輸原理

Stream 在 IPC 的運用

事實上當知道它也是運用拷貝來將資料進行傳輸時,那就代表它也可以使用 stream 來處理它。

原理會變成如下圖 6 所示 :

圖 6 : stream 在 ipc 的傳輸原理

Android Binder

這裡咱們要順到來看一個東西,那就是『 Andorid Binder 』,它是在 android 的系統上所提供的另一種 ipc 通信方案。

傳統的 ipc 通信基本上都一定要通常以下幾次拷貝,來將資料進行傳輸,如下範例。

假設 process A 要傳資料到 process B

  • 從 process A 拷貝資料到內核緩衝區 ( 1 次 )。
  • 從內核緩衝區拷貝到 process B 的用戶緩衝區 ( 2 次 )。

而 binder 這個機制只要 1 次拷貝。

  • binder 在內核緩衝區在另外創建接受方的緩衝區。
  • 從 process A 拷貝資料到內核緩衝區 ( 1 次 )。
  • 接受方的緩衝區與內核緩衝區有 map ( binder 處理)。
  • 由於 process B 某個記憶體有 map 到 binder 開的內核接受方的位置,所以就可以取得 process A 的資料囉。

圖 7 : android binder 的資料傳輸原理

為啥 process B 會知道 binder 開的記憶體位置呢 ? 因為在開始前 A 與 B 都需要對 binder 的 servcie manager 註冊喔,所以才會知道呢。

這裡就只是淺淺的談一下它的概念,詳細的知識有興趣的可以去找找。

~ 小知識 1 ~ 這裡有提到記憶體 map 的東西,它所實現的核心技術就是之前咱們在零拷貝有提到的 linux 的 mmap 別忘了它,它在性能的世界中佔了很重要的地位。

30-05 之應用層的 I / O 加速 - 零拷貝 ( Zero Copy )

~ 小知識 2 ~ binder 是 android 內核所提供的東西喔。

簡單的小範例 ( Nodejs )

最後咱們來簡單的使用 nodejs 來寫個小範例。比較詳細的 nodejs stream 操作可以參考此篇文章。

範例 1 : 一個 http 請求影片資源,並回傳回去

下面範例你可以想成,從硬碟到網路會有一條 stream 的感覺。

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'video/avi' });
    fs.createReadStream('aaa.avi').pipe(res)
        .on('finish', () => {
            console.log('done');
        });
});

server.listen(3000, () => {
    console.log('server up !');
});

範例 2 : Process A 以串流傳送文字給 Process B

對不起我實在有點懶,所以直接拿這個套件來使用。以下為它的網路範例程式碼。

Server 程式碼 ( 就是 Process A )

// Server

import { getServer, NodeIpcServerDuplex } from 'stream-node-ipc';
 
const someNodeIPCConfigToOverride = { maxConnections: 12 };
const ipcClient = getServer('magne4000-test-worker', someNodeIPCConfigToOverride);
 
const newClientConnection = (_data: Buffer, socket: Socket) => {
  const duplex = new NodeIpcServerDuplex(ipcClient, socket);
  duplex.write('Hi Mark ~'); // 傳送資料給 client (Process B)
};
 
ipcClient.on('data', newClientConnection);

Client 程式碼 ( 就是 Process B )

import { getClient, NodeIpcClientDuplex } from 'stream-node-ipc';
 
const someNodeIPCConfigToOverride = { logger: console.log };
const ipcClient = getClient('magne4000-test-worker', 'myClientId', someNodeIPCConfigToOverride);

const duplex = new NodeIpcClientDuplex(ipcClient);
duplex.on('data', (data) => {
  // 收到從 server (process A) 來的資料
  // Hi Mark ~
})

結論與心得

本篇文章中,咱們學習到了 stream 這個每一個程式語言都有提供的操作的核心原理 :

分段傳送,為了不讓緩衝區爆炸

並且也簡單的看了一下 ipc 的一些與 stream 相關機制與,然後這裡也看將 android binder 也抓出來說說,因為它的觀念很有意思。

最後這裡簡單的說一下。

stream 這東西咱們每個人一定都在不知不覺有使用到,如果只是會用,但不知道理解為什麼要這樣用,為什麼有些 lib 底層是有時用 stream 有時用一般拷貝方式,或需現階段工作上還可以活的好好的,但是未來出事了,沒有人可以救你的。

不斷的追求為什麼,才知學習正道。

參考資料

comments powered by Disqus