JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

匯流排與互連

處理器與記憶體若沒有東西把位元在它們之間、以及與外界之間搬來搬去,就毫無用處。本篇追蹤那些線——從大家搶著用的共享匯流排,到取而代之、像 PCIe 那樣的點對點連結——並說明 CPU 一開始究竟是怎麼跟磁碟說上話的。

我們現在的位置:沒人畫出來的那些線

上一篇誠實地論證了:I/O——而非 CPU——往往才是真正的瓶頸,並給了你三把尺來評斷任何裝置:吞吐量、延遲與可靠度。但它留下一個懸而未決的問題。處理器坐在一顆晶片上;磁碟、網路卡與鍵盤則坐在完全不同的地方。位元不會瞬間移動。一定要有某種實體的東西把它們搬過去,而那東西本身就有它自己的吞吐量與延遲,悄悄地替它後面每個裝置設下了天花板。本篇講的就是那些線,以及它們遵守的規則。

把整台機器想成一棟建築。CPU 與主DRAM共用中央附近一條快速的私人走廊。其餘一切——硬碟、網路卡、GPU——住在更外圍的房間,由寬窄與長短各異的走道相連。匯流排互連就是那些走道之一:一束電線加上一套協定,規定輪到誰說話、位址怎麼送、資料與確認訊息怎麼回來。系統設計的藝術,就在於把每條走道的寬度與長度,配上流經其中的流量。

共享匯流排:一條走道,大家輪流

最古老的設計是共享匯流排:一組電線,許多裝置全都並排接上去,像一條電話的合用線路。它便宜得令人愉快——加一個新裝置只要把它夾到既有的線上即可——而且讓廣播變得自然,這正是多處理器那一階的窺探式快取一致性方案會倚賴共享匯流排的原因:它們才能偷聽到每一筆交易。一條匯流排通常承載三組邏輯線:位址線說在哪裡、資料線載著是什麼、控制線載著何時哪一種(讀或寫)。

但合用線路有個致命弱點:一次只能一人說話,否則訊息會撞成雜訊。所以共享匯流排需要一個仲裁器——一個裁判,每一輪只把線授予恰好一個裝置——其餘每個裝置都得等。更糟的是物理跟你作對。一長串掛著許多裝置的電線,行為像一根重載的天線:訊號反射、歪斜、抹糊,所以匯流排越長、越忙,能安全運作的時脈就越低。這就是為什麼你不能單純地把共享匯流排無止境地加寬又加速;頻寬與乾淨的訊號傳遞往相反方向拉扯。

點對點與 PCIe:許多條私有走道

現代的答案,是放棄共享的合用線路,改用點對點連結:每個裝置都有自己專屬的電線,直接連到一個中央交換器,於是兩個裝置可以同時對話,不必由仲裁器配給單一走道。PCI Express(PCIe)是今日電腦裡最主流的例子。關鍵在於:一條 PCIe 連結並不是一條又寬又胖的並列匯流排,而是一組窄窄的通道(lane),每條通道是一對電線,把位元一個接一個(串列地)送出。插槽以 x1、x4、x8、x16 描述——也就是通道數——連結的頻寬就隨著這個數字成比例放大。

這種「串列又窄」的選擇會讓初學者意外,因為直覺說又寬又並列的匯流排應該勝過又細又串列的。串列之所以勝出,誠實的理由正是第二節的物理。在寬的並列匯流排裡,一個字組的各個位元沿著許多條線狂奔,而它們必須在同一瞬間全部抵達;高速下它們會步調走樣(歪斜),這就限制了時脈。單一條串列通道沒有任何東西需要跟它同步,所以可以被推到快得多的時脈,而你只要平行跑許多條互不相干的通道就能補回寬度。線少、每條快很多,勝過許多條被逼著齊步前進的線。

CPU 究竟如何跟裝置說話

電線載著位元,但程式要怎麼定址一顆磁碟?它辦不到——CPU 的指令集只懂得如何從記憶體位址載入、以及存入記憶體位址。訣竅就是讓裝置假裝成記憶體。每個真實裝置前面都站著一個裝置控制器:裝置那側的一顆小晶片,對外露出少數幾個暫存器——一個命令暫存器、一個狀態暫存器、一個資料暫存器。透過記憶體映射 I/O,那些暫存器被接到特定的實體位址上。存入位址 0x1F0 也許表示「控制器,這裡有一個位元組要寫」;從 0x1F7 載入也許表示「控制器,你的狀態如何?」。

這做法美在省:完全不需要任何新指令。你在 ISA 那一階學的同一組載入與存入、同一套位址轉換硬體,就能觸及控制器。作業系統只要把那些位址標示為使用者程式禁區——這是一個保護問題——於是只有核心的驅動程式能去戳它們。CPU 把一個命令寫進命令暫存器,控制器便去做那些緩慢的實體工作:移動磁頭、為快閃記憶體充電,或驅動網路線。

Memory-mapped I/O: the controller's registers ARE addresses

  physical address    meaning when accessed
  ----------------    ---------------------------------------
  0x1F0  data         store -> byte to send; load -> byte read
  0x1F2  count        how many blocks to transfer
  0x1F3..6 sector     which disk block (the address on disk)
  0x1F7  cmd/status   store -> issue command;
                      load  -> bit7 BUSY, bit3 DRQ, bit0 ERR

  driver: store sector + count, store READ into 0x1F7,
          then watch the status register for 'ready'...
          (HOW it watches is the next guide: poll? interrupt? DMA?)

注意最後一行那個懸念。CPU 已經發出命令;現在它必須查出裝置何時做完,因為一次磁碟讀取要花上數百萬個 CPU 週期。在迴圈裡狂讀狀態暫存器、做完時發出中斷、或把整段傳輸交給一個 DMA 引擎,就是那三種策略——也正是下一篇要談的。匯流排與記憶體映射暫存器是舞台;輪詢、中斷與 DMA 則是在台上演出的戲。

誠實的提醒,以及接下來通往何處

有幾個值得記住的真相。第一,一個快裝置接在慢互連後面,是浪費錢——走道封住了房間的上限。一顆 NVMe SSD 接在單一條 PCIe 通道上,會比同一顆磁碟接在四條通道上表現差得多,而其他什麼都沒變。第二,頻寬與延遲是各自獨立的美德:PCIe 給你龐大的吞吐量,但每一筆個別交易仍要付一段真實的來回延遲,這就是為什麼把許多小請求綁成一筆大傳輸幾乎總是划算。第三,在競爭之下,共享走道會悄悄拉高尾端延遲——多數請求很快,但那些不巧在匯流排忙碌時抵達的倒楣鬼,等的時間遠超平均,而在資料中心裡,使用者感受到的正是那些慢的尾巴。

更大的圖像是:同一個點子在每一個尺度上重演。在晶片內,核心透過記憶體通道與一個記憶體控制器觸及記憶體;跨越一塊主機板,裝置透過 PCIe 互相連接;跨越一棟建築,機器透過資料中心網路與一個機架頂端交換器彼此相連。每一層的問題都一模一樣——共享媒介還是點對點?我們怎麼仲裁?吞吐量與延遲各是多少?——而趨勢處處相同:離開單一共享匯流排,走向許多條交換式的點對點連結。你現在看過了那些線。下一篇會讓它們活起來,回答 CPU 與裝置如何協調,又不讓 CPU 把生命浪費在空等上。