iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Software Development

從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局系列 第 25 篇

[Day 25] HFT Networking Infrastructure: Shared Memory IPC

  • 分享至 

  • xImage
  •  

在對延遲極度敏感的 HFT 設計中,為了將通訊延遲壓低至亞微秒級,POSIX 共享記憶體 IPC 成為唯一的解決方案,也是很多證券即時交易系統的重要框架。本文將從底層原理到硬體層面最佳化,深入探討如何打造極低延遲的通訊機制。

1. 共享記憶體的終極解法 —— Zero Kernel Involvement

為了理解共享記憶體的必要性,首先需看清 Unix Domain Socket 等傳統 IPC 的瓶頸。

  • Context Switch:發送端呼叫 write(),CPU 切換到 kernel mode;接收端呼叫 read(),CPU 再次切換。即便是最佳化後的 Linux 核心,一次 context switch 的基本開銷也在數百奈秒到 1 微秒之間。

  • Memory Copy:資料必須經歷「發送端 User Buffer → Kernel Socket Buffer → 接收端 User Buffer」至少兩次的記憶體複製。對於大型資料,這會嚴重消耗記憶體頻寬。

  • Scheduler Jitter:如果接收端在等待資料時進入 blocked 睡眠狀態,當資料到達時,作業系統排程器需要喚醒該行程。這個 thread wakeup 具有極大的不確定性,是 tail latency 出現 spike 的主要原因。

而 POSIX 共享記憶體則有以下優勢。

  • 初始化階段:透過 shm_open 與 mmap 允許多個獨立的行程將同一塊實體記憶體分頁映射到各自的 virtual address space。

  • 資料傳輸階段:一旦映射完成,整個通訊過程完全不需要經過 Linux kernel,並且能達到 zero-copy。行程 A 寫入資料時,是直接透過 CPU 的 MMU 進行地址轉換並寫入實體記憶體;透過 MESI 等 cache coherency 協定,行程 B 則能立刻看到該記憶體位置的所有變動。通訊延遲直接等同於 CPU 存取 DRAM 或 CPU cache 的時間,通常僅需數十奈秒。

2. 亞微秒級 Shared Memory IPC 的架構設計 —— SPSC 無鎖環形緩衝區

要真正發揮共享記憶體的極致效能,因為底層仍可能陷入核心 futex 導致阻塞,故不能用傳統的 pthread_mutex。我們必須基於 ring buffer 或 circular queue 來設計一套 Single-Producer Single-Consumer (SPSC) 的 lock-free 架構。

在共享記憶體區塊中,通常會劃分為控制表頭與資料緩衝區:
+-------------------------------------------------------------------------------------------+
|⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Shared Memory⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀|
+---------------------------------------------+---------------------------------------------+
|⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Char⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀|⠀⠀⠀⠀⠀⠀⠀Data Region⠀⠀⠀⠀⠀⠀⠀|
|⠀- Write Index (atomic<uint64_t>)⠀|⠀+------------------------------------+⠀|
|⠀- Read Index  (atomic<uint64_t>)⠀|⠀|⠀Slot 0⠀|⠀Slot 1⠀|⠀...⠀⠀⠀⠀⠀|⠀|
|⠀- Padding to prevent False Sharing⠀⠀|⠀+------------------------------------+⠀|
+---------------------------------------------+---------------------------------------------+

  • Write Index (Tail):由 Producer 更新,指出下一個可寫入的位置。
  • Read Index (Head):由 Consumer 更新,指出下一個可讀取的位置。
  • Data Slots:固定大小的資料區塊,避免動態記憶體分配,達到 zero allocation 的效果。

3. 核心底層技術與硬體最佳化實踐 —— 所學居然能串在一起!

為了將延遲壓制在亞微秒級,必須結合 shared memory IPC 的幾項關鍵技術。而這些觀念,正是我們在前面章節逐步累積的知識點。

while (write_index.load(std::memory_order_relaxed) == current_read_idx) {
    // 或是 _mm_pause() 最佳化 CPU 管道
    #if defined(__x86_64__) || defined(_M_X64)
    _mm_pause();
    #endif'
}

_mm_pause() 指令非常關鍵,它能告訴 CPU 這是一個忙碌等待迴圈,優化處理器的 pipeline,避免耗電與過熱,同時降低退出迴圈時的延遲。

同時可以使用 Acquire-Release 語義,保證在讀取 write_idx 之後,接下來讀取 Slot 資料時絕對能看到 Producer 寫入的最新內容。

auto current_write_idx = write_index.load(std::memory_order_acquire);

4. 作業系統與硬體層級的低延遲優化

在 HFT 等極低延遲場景中,除了 lock-free 及 ring buffer 的演算法設計外,作業系統與硬體配置也會影響 IPC 的最終延遲。實務上通常會搭配以下 tuning 技巧。

  • CPU Core Pinning:使用 pthread_setaffinity_np() 將 Producer 與 Consumer 綁定至指定 CPU core,降低 thread migration 與 cache locality 變化造成的延遲。
  • NUMA Awareness:在 multi-socket 系統中,存取本機 CPU 節點連接的記憶體很快,但透過 UPI/QPI 總線存取另一個 CPU 節點的遠端記憶體則慢得多。為了確保 shared memory 儘量配置於 Producer 與 Consumer 所在的 NUMA Node,避免不必要的遠端記憶體存取,以 numactl 工具或 move_pages 系統呼叫來強制記憶體親和性。
  • Hugepages:當 IPC 的 ring buffer 很大時,會頻繁觸發 Translation Lookaside Buffer (TLB) Miss,導致 CPU 必須花費數百奈秒去查記憶體分頁表。透過 hugetlbfs 映射共享記憶體配置 hugepage,大幅減少分頁表層級能將 TLB Miss 機率降到接近零。
  • CPU Isolation:透過 isolcpus 等機制降低其他系統工作對關鍵 CPU core 的干擾。
  • Cache Line Alignment:避免 Producer 與 Consumer 修改同一 cache line 而產生 false sharing。

這些技術本身並不是 Shared Memory IPC 的必要條件,而是在既有的 shared memory 加 lock-free 設計之上,進一步降低 latency 與 latency jitter 的系統級優化手段。

5. 實戰!Shared Memory IPC 程式碼架構

在實作上,建立一個 POSIX Shared Memory IPC 的典型步驟如下。

  • 建立並初始化:
int fd = shm_open("/shm_channel", O_CREAT | O_RDWR, 0666);
ftruncate(fd, SHM_SIZE);
void* ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  • 結構體映射:將 ptr 強制轉型,接著把 mmap() 回傳的記憶體位址視為預先設計好的 ring buffer 結構。
*struct RingBuffer {
    // Read / Write index
    // Buffer slots
    // Synchronization-related data
};

RingBuffer* ring = static_cast<RingBuffer*>(ptr);

在此請特別注意,結構體的記憶體布局必須固定,Producer 與 Consumer 應使用完全一致的資料結構。

總結來說,POSIX shared memory 加上 lock-free ring buffer 是一種能夠實現極低 IPC 延遲與高吞吐量的架構;透過 busy polling、CPU affinity、NUMA 與 cache 最佳化,即使無法保證絕對的 zero jitter,依舊能進一步降低延遲與抖動。這也是它適合用於低延遲行情傳遞、process-to-process 資料交換,以及部分高效能交易系統架構的重要原因。


上一篇
[Day 24] HFT Networking Infrastructure: Non-Blocking I/O
下一篇
[Day 26] 進度存檔 | Low-Latency 主王都冒險日誌
系列文
從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言