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

輪詢、中斷與 DMA

你已經知道裝置是透過控制器與暫存器來溝通的;接下來這個問題,比任何問題都更決定 I/O 的效能:當那台慢吞吞的裝置在工作時,CPU 在幹嘛?我們會走過三個答案——靠輪詢來忙等、被中斷在肩膀上拍一下,以及把整段傳輸交給 DMA 引擎,好讓 CPU 乾脆整個離開現場。

真正的問題:裝置工作時,CPU 在做什麼?

在上一篇導覽裡你學到,裝置不會直接跟 CPU 講話。它躲在一個控制器後面——一塊小電路,對外露出幾個暫存器(通常是狀態暫存器、控制暫存器和資料暫存器),CPU 透過 I/O 埠或記憶體映射 I/O 去讀寫它們。有了這套詞彙,一個更銳利的問題就浮現了,而它是整個 I/O 領域裡最重要的問題。鍵盤、磁碟、網路卡——它們每一個跟 CPU 比起來都慢得令人心痛。現代處理器一秒鐘能執行數十億條指令;一次磁碟讀取可能要好幾毫秒;一次按鍵可能要好幾秒才來。所以當 CPU 啟動一次 I/O、而裝置開始它慢吞吞的工作時,CPU 實際上多出了一大段空白時間。它要拿那段時間做什麼?這一個問題,有三個經典答案:輪詢、中斷與 DMA。

把它想成叫外送。你打電話點餐——那就是 CPU 把指令寫進裝置的控制暫存器。現在廚房慢慢地煮,而你可以選擇要怎麼度過這段等待。你可以全程站在櫃台前盯著廚師看。你可以回去坐著,等好了讓他們搖鈴叫你。或者你可以派一個跑腿的去拿,在他把餐放到你桌上之前,你完全不去想吃的這回事。這三種策略,正好對應到輪詢中斷DMA——而且就跟吃晚餐一樣,它們之間的差別幾乎完全在於:這段等待,吃掉了你多少自己的注意力。

程式化 I/O:輪詢,也就是盯著櫃台看

最單純的方案是搭配輪詢程式化 I/O。在這裡,CPU 自己搬移每一個位元組;而為了知道裝置何時就緒,它在一個緊湊的迴圈裡不斷重讀狀態暫存器。把狀態暫存器想成有一個忙碌位元(busy bit):裝置在工作時這個位元是 1,做完了就掉回 0。CPU 一遍又一遍地問「你好了嗎?你好了嗎?你好了嗎?」問上好幾千次,直到終於看見那個位元歸零,才搬移一塊資料,然後又繞回去問下一塊。這種反覆檢查,正是「盯著櫃台看」的策略:CPU 完全被佔住,除了盯著看以外什麼也沒做。

  // Polling loop: programmed I/O writing one byte
  write command into the control register      // place the order
  loop:
      read the status register                  // "are you ready?"
      if busy bit is still 1, goto loop         // ...spin, spin, spin
  write the next byte into the data register    // device is ready: hand it over
  // ...repeat the whole loop for every byte
輪詢迴圈的虛擬碼:CPU 在狀態暫存器的忙碌位元上空轉,不做任何有用的事,直到裝置準備好接收下一個位元組。

輪詢有一個真實的優點:它簡單到不行,而且延遲極低,因為裝置一翻轉那個位元,CPU 幾乎立刻就會注意到。對於一台又快又幾乎總是就緒的裝置,這種靈敏其實能贏。但當裝置很慢時,代價就慘烈了。CPU 在那個迴圈裡空轉的每一個週期,都是從機器上其他每一個行程那裡偷來的週期。這種浪費的空轉就是忙等(busy-waiting)——你在同步那一階早就見過它的表親:自旋鎖為了等一把鎖而燒掉週期。用輪詢去等磁碟,每個請求可能浪費數百萬個週期——就像在外送櫃台前僵立十分鐘,而你背後排起了一整列餓著肚子的人。

中斷:讓裝置去按門鈴

解法是別再盯著看,讓裝置在做完時主動告訴我們。這就是中斷驅動 I/O,而它背後的機制就是中斷——整台電腦的門鈴。CPU 啟動 I/O 之後,並不去跑迴圈;它就乾脆跑去執行其他行程。當裝置終於做完,控制器會在一條專用的線上對 CPU 升起一個電氣訊號。這個訊號逼使 CPU 暫停它此刻正在做的任何事、存下剛好夠之後回來用的狀態、跳到一小段處理該裝置的核心常式,然後就像什麼都沒發生過一樣繼續被打斷的工作。CPU 跑去坐下了;廚房搖了鈴;CPU 只有在真的有餐可以拿時才站起來。

留意這如何徹底改變了這筆帳。慢裝置工作的那一整段時間,不再被空轉浪費掉——它被還給了其他行程。這正是為什麼你在排程那一階見過的 CPU–I/O 爆發週期能夠成立:一個發出 I/O 的行程可以被擱到阻塞狀態,排程器可以去跑別人,而中斷就是那聲鬧鐘,宣告「這個行程的 I/O 做完了——把它變回就緒」。中斷正是把 I/O 與排程縫合在一起的那道接縫。沒有它,一次慢吞吞的讀取就會凍結整台機器;有了它,慢速 I/O 與有用的工作得以重疊。

DMA:派個跑腿的,好讓 CPU 離開現場

中斷解決了等待的問題,卻留下一筆頑固的代價沒動到。就算有了中斷,在單純的程式化 I/O 裡,搬移每一個位元組、一次一個地通過資料暫存器的,仍然是 CPU。對一個 4 KB 的磁碟區塊來說那是好幾千趟,而對一次大檔案傳輸來說,那是把 CPU 的精力巨量地浪費在其實只是「複製」的工作上。想像那個替你拿餐的跑腿——但你卻堅持要親手把每一根薯條從廚房一根一根端到桌上。這正是直接記憶體存取(DMA)要補上的缺口。

DMA 控制器是一個小小的專用引擎,能夠自己讀寫主記憶體,不必由 CPU 去操控每一個位元組。CPU 的工作縮成一段簡短的設定:告訴 DMA 控制器來源、記憶體中的目的位址,以及共有多少位元組——然後就走開去跑其他行程。接著 DMA 控制器與裝置就自己把整塊資料直接串流進 RAM。CPU 只會在整段傳輸完成時,被單獨一次中斷打擾。這就是外送跑腿的完整版:你下單、講好份量,搬運全交給跑腿,而你只在整份餐落到桌上時才被打斷一次。

  1. 驅動程式替 DMA 控制器設定好來源裝置、RAM 中的目的位址與位元組數量,接著 CPU 就空出來去跑其他行程。
  2. DMA 控制器與裝置把整塊資料一個位元組接一個位元組地直接搬進記憶體,完全不需要 CPU 插手。
  3. 傳輸結束時,DMA 控制器升起單獨一次中斷,告訴 CPU 資料現在已經躺在 RAM 裡了。
  4. CPU 的中斷處理常式做一點點記帳工作——喚醒那個等待中的行程——然後回到它原本在做的事。

在三者之間做選擇

我們很容易想把這三者從最差排到最好,但那是錯的圖像。在不同條件下,每一個都是對的工具。當裝置又快又幾乎總是就緒時,輪詢最出色,因為迴圈只空轉一瞬間,而你省下了全部的中斷開銷——許多細小的狀態檢查,以及一些高速驅動程式,確實是刻意去輪詢的。當事件相對稀少、而你想讓 CPU 在事件之間空出來時,中斷最出色,這涵蓋了像鍵盤、滑鼠這類大多數日常裝置。每當你必須搬移一大塊資料時,DMA 最出色,因為「替 CPU 省下複製好幾千個位元組」這件事,讓其他一切都相形見絀。真實系統會自由地混用三者:一次磁碟讀取可能由 CPU 設定、由 DMA 執行、再由一次中斷宣告完成,全在同一個操作裡。

把這三者綁在一起的那條線,就是貫穿這整個領域的同一個主題:裝置很慢、CPU 很珍貴,而好的設計,講的是別浪費 CPU 的注意力。輪詢花掉注意力,換來簡單與低延遲。中斷花掉每個事件一點點開銷,贏回等待的時間。DMA 花掉一次性的設定,贏回複製的時間。守著這副眼鏡,這一階剩下的部分就會讀得很順。下一篇導覽會把中斷本身撬開——CPU 如何透過中斷向量知道該跑哪個處理常式,以及為什麼工作會被切成快速的上半部與延後的下半部——因為這三種風格,到頭來每一個都倚賴那個機制。