Stream 這個東東,基本上在每一個語言你都看的到,而今天我們將要深入的來理解它到底是什麼東西,並且它在一些 I/O 操作上可以幫助我們解決什麼事情。
本篇文章將分為以下幾個章節
stream 它是一種技術,基本上專門用來傳輸資料用。
咱們先來看看傳統上,咱們如果要從硬碟拿個檔案是如何處理。
基本上流程如下圖 1 所示。
圖 1 : 傳統的資料傳輸流程
而重點在於這裡 :
硬碟資料 copy 到內核緩衝區,接下來再從內核緩衝區 copy 到用戶緩衝區。
然後問題出在於 :
如果資料很大會發生什麼事呢 ?
基本上結果就如下圖 2 所示,有可能在內核緩衝或用戶緩衝,就爆掉了,因為記憶體是整份 copy 過去,如果你硬碟資料有 10 gb,那就代表,要將這 10 gb 的資料 copy 到內核緩存,再 copy 到用戶緩存,這時後用戶進程才能拿到它。
但正常來說,不論是內核緩存或用戶緩存都一定要大小限制,如果直接來個 10gb 資料,那一定會炸掉的。
圖 2 : 傳統資料傳輸的問題
而正也是這個原因,才有 stream 這個技術的誕生。
~ 小提醒 ~
緩衝區就是指某個記憶體區塊,大部份在資料傳輸的時後,每一個地方的應用、包含內核都會在某個記憶體區塊,開啟一些緩衝區,用來存放等等進來的資料。
stream 的基本原理如下圖 3 所示,它不是直接一次將 10gb 的資料拷則到內核緩存後,再直接全部拷則到用戶緩存,它是一點一點的慢慢拷則,每當拷貝 100 mb ( 這只是範例大小 ) 到內核時就繼續拷貝到用戶,然後用戶再直接將它拿來使用,而當使用完,用戶就是將記憶體清除掉,就不會有什麼爆炸,或記憶體爆漲的問題。
圖 3 : stream 原理
從上圖原理事實上會發現一個事情。
如果它是每 100 mb 就會拷貝到用戶緩存,然後用戶在將它處理掉,那這樣是不是代表每進行一次這個就會進行一次上下文切換呢 ?
嗯對。
不過這裡簡單說明一下,它的上下文切換不會比進程至進程的上下文切換還耗資源,因為它不用處理下一個進程的東西,這也代表不需要記錄與載入其它進程的資訊,比較圖如下圖 4 所示 。這種切換比較常被稱為『 特權模式切換 』,不過事實上它也的確也換是有進行上下文切換。
圖 4 : 特權模式切換與進程上下文切換比較
接下來咱們來說說 stream 在 ipc 通信的一些原理,咱們先從 ipc 開始來談談。
比較白話的說法為 :
它是作業系統提供 process 與 process 的傳輸資料的方式
linux 基本上有提供以下幾種方式 :
它們的運作原理基本上就如下圖 5 所示,流程如下 :
這就是 IPC 基本的運作原理,這裡就先不探討不同 IPC 方法有啥不同,只要先知道原理相同就好。
圖 5 : ipc 的傳輸原理
事實上當知道它也是運用拷貝來將資料進行傳輸時,那就代表它也可以使用 stream 來處理它。
原理會變成如下圖 6 所示 :
圖 6 : stream 在 ipc 的傳輸原理
這裡咱們要順到來看一個東西,那就是『 Andorid Binder 』,它是在 android 的系統上所提供的另一種 ipc 通信方案。
傳統的 ipc 通信基本上都一定要通常以下幾次拷貝,來將資料進行傳輸,如下範例。
假設 process A 要傳資料到 process B
而 binder 這個機制只要 1 次拷貝。
圖 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 stream 操作可以參考此篇文章。
下面範例你可以想成,從硬碟到網路會有一條 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 !');
});
對不起我實在有點懶,所以直接拿這個套件來使用。以下為它的網路範例程式碼。
// 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);
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 有時用一般拷貝方式,或需現階段工作上還可以活的好好的,但是未來出事了,沒有人可以救你的。
不斷的追求為什麼,才知學習正道。