電腦最魔幻的事情就是它能感知到你的思想

我們之前的文章提到了作業系統的三個抽象,它們分別是進程、地址空間和文件,除此之外,作業系統還要控制所有的 I/O 設備。作業系統必須向設備發送命令捕捉中斷處理錯誤。它還應該在設備和作業系統的其餘部分之間提供一個簡單易用的介面。作業系統如何管理 I/O 是我們接下來的重點。

不同的人對 I/O 硬體的理解也不同。對於電子工程師而言,I/O 硬體就是晶片、導線、電源和其他組成硬體的物理設備。而我們程式設計師眼中的 I/O 其實就是硬體提供給軟體的介面,比如硬體接受到的命令、執行的操作以及回饋的錯誤。我們著重探討的是如何對硬體進行編程,而不是其工作原理。

I/O 設備

什麼是 I/O 設備?I/O 設備又叫做輸入/輸出設備,它是人類用來和電腦進行通訊的外部硬體。輸入/輸出設備能夠向電腦發送數據(輸出)並從電腦接收數據(輸入)

I/O 設備(I/O devices)可以分成兩種:塊設備(block devices)字元設備(character devices)

塊設備

塊設備是一個能存儲固定大小塊資訊的設備,它支援以固定大小的塊,扇區或群集讀取和(可選)寫入數據。每個塊都有自己的物理地址。通常塊的大小在 512 – 65536 之間。所有傳輸的資訊都會以連續的塊為單位。塊設備的基本特徵是每個塊都較為對立,能夠獨立的進行讀寫。常見的塊設備有 硬碟、藍光光碟、USB 盤

與字元設備相比,塊設備通常需要較少的引腳。

塊設備的缺點

基於給定固態存儲器的塊設備比基於相同類型的存儲器的位元組定址要慢一些,因為必須在塊的開頭開始讀取或寫入。所以,要讀取該塊的任何部分,必須尋找到該塊的開始,讀取整個塊,如果不使用該塊,則將其丟棄。要寫入塊的一部分,必須尋找到塊的開始,將整個塊讀入記憶體,修改數據,再次尋找到塊的開頭處,然後將整個塊寫回設備。

字元設備

另一類 I/O 設備是字元設備。字元設備以字元為單位發送或接收一個字元流,而不考慮任何塊結構。字元設備是不可定址的,也沒有任何尋道操作。常見的字元設備有 印表機、網路設備、滑鼠、以及大多數與磁碟不同的設備

下面顯示了一些常見設備的數據速率。

設備控制器

首先需要先了解一下設備控制器的概念。

設備控制器是處理 CPU 傳入和傳出訊號的系統。設備通過插頭和插座連接到電腦,並且插座連接到設備控制器。設備控制器從連接的設備處接收數據,並將其存儲在控制器內部的一些特殊目的暫存器(special purpose registers) 也就是本地緩衝區中。

特殊用途暫存器,顧名思義是僅為一項任務而設計的暫存器。例如,cs,ds,gs 和其他段暫存器屬於特殊目的暫存器,因為它們的存在是為了保存段號。eax,ecx 等是一般用途的暫存器,因為你可以無限制地使用它們。例如,你不能移動 ds,但是可以移動 eax,ebx。 通用目的暫存器比如有:eax、ecx、edx、ebx、esi、edi、ebp、esp 特殊目的暫存器比如有:cs、ds、ss、es、fs、gs、eip、flag 」

每個設備控制器都會有一個應用程式與之對應,設備控制器通過應用程式的介面通過中斷與作業系統進行通訊。設備控制器是硬體,而設備驅動程式是軟體。

I/O 設備通常由機械組件(mechanical component)電子組件(electronic component)構成。電子組件被稱為 設備控制器(device controller)或者 適配器(adapter)。在個人電腦上,它通常採用可插入(PCIe)擴展插槽的主板上的晶片或印刷電路卡的形式。

機械設備就是它自己,它的組成如下

控制器卡上通常會有一個連接器,通向設備本身的電纜可以插入到這個連接器中,很多控制器可以操作 2 個、4 個設置 8 個相同的設備。

控制器與設備之間的介面通常是一個低層次的介面。例如,磁碟可能被格式化為 2,000,000 個扇區,每個扇區 512 位元組。然而,實際從驅動出來的卻是一個串列的比特流,從一個前導符(preamble)開始,然後是一個扇區中的 4096 位,最後是一個校驗和ECC(錯誤碼,Error-Correcting Code)。前導符是在對磁碟進行格式化的時候寫上去的,它包括柱面數和扇區號,扇區大小以及類似的數據,此外還包含同步資訊。

控制器的任務是把串列的位流轉換為位元組塊,並進行必要的錯誤校正工作。位元組塊通常會在控制器內部的一個緩衝區按位進行組裝,然後再對校驗和進行校驗並證明位元組塊沒有錯誤後,再將它複製到記憶體中。

記憶體映射 I/O

每個控制器都會有幾個暫存器用來和 CPU 進行通訊。通過寫入這些暫存器,作業系統可以命令設備發送數據,接收數據、開啟或者關閉設備等。通過從這些暫存器中讀取資訊,作業系統能夠知道設備的狀態,是否準備接受一個新命令等。

為了控制暫存器,許多設備都會有數據緩衝區(data buffer),來供系統進行讀寫。例如,在螢幕上顯示一個像素的常規方法是使用一個影片 RAM,這一 RAM 基本上只是一個數據緩衝區,用來供程式和作業系統寫入數據。

那麼問題來了,CPU 如何與設備暫存器和設備數據緩衝區進行通訊呢?存在兩個可選的方式。第一種方法是,每個控制暫存器都被分配一個 I/O 埠(I/O port)號,這是一個 8 位或 16 位的整數。所有 I/O 埠的集合形成了受保護的 I/O 埠空間,以便普通用戶程式無法訪問它(只有作業系統可以訪問)。使用特殊的 I/O 指令像是

IN REG,PORT  

CPU 可以讀取控制暫存器 PORT 的內容並將結果放在 CPU 暫存器 REG 中。類似的,使用

OUT PORT,REG  

CPU 可以將 REG 的內容寫到控制暫存器中。大多數早期電腦,包括幾乎所有大型主機,如 IBM 360 及其所有後續機型,都是以這種方式工作的。

控制暫存器是一個處理器暫存器而改變或控制的一般行為 CPU 或其他數字設備。控制暫存器執行的常見任務包括中斷控制,切換定址模式,分頁控制和協處理器控制。 」

在這一方案中,記憶體地址空間和 I/O 地址空間是不相同的,如下圖所示

指令

IN R0,4  

MOV R0,4  

這一設計中完全不同。前者讀取 I/O埠 4 的內容並將其放入 R0,而後者讀取存儲器字 4 的內容並將其放入 R0。這些示例中的 4 代表不同且不相關的地址空間。

第二個方法是 PDP-11 引入的,

什麼是 PDP-11?

它將所有控制暫存器映射到記憶體空間中,如下圖所示

記憶體映射的 I/O是在 CPU 與其連接的外圍設備之間交換數據和指令的一種方式,這種方式是處理器和 IO 設備共享同一記憶體位置的記憶體,即處理器和 IO 設備使用記憶體地址進行映射。

在大多數系統中,分配給控制暫存器的地址位於或者靠近地址的頂部附近。

下面是採用的一種混合方式

這種方式具有與記憶體映射 I/O 的數據緩衝區,而控制暫存器則具有單獨的 I/O 埠。x86 採用這一體系結構。在 IBM PC 兼容機中,除了 0 到 64K – 1 的 I/O 埠之外,640 K 到 1M – 1 的記憶體地址保留給設備的數據緩衝區。

這些方案是如何工作的呢?當 CPU 想要讀入一個字的時候,無論是從記憶體中讀入還是從 I/O 埠讀入,它都要將需要的地址放到匯流排地址線上,然後在匯流排的一條控制線上調用一個 READ 訊號。還有第二條訊號線來表明需要的是 I/O 空間還是記憶體空間。如果是記憶體空間,記憶體將響應請求。如果是 I/O 空間,那麼 I/O 設備將響應請求。如果只有記憶體空間,那麼每個記憶體模組和每個 I/O 設備都會將地址線和它所服務的地址範圍進行比較。如果地址落在這一範圍之內,它就會響應請求。絕對不會出現地址既分配給記憶體又分配給 I/O 設備,所以不會存在歧義和衝突。

記憶體映射 I/O 的優點和缺點

這兩種定址控制器的方案具有不同的優缺點。先來看一下記憶體映射 I/O 的優點。

  • 第一,如果需要特殊的 I/O 指令讀寫設備控制暫存器,那麼訪問這些暫存器需要使用彙編程式碼,因為在 C 或 C++ 中不存在執行 INOUT指令的方法。調用這樣的過程增加了 I/O 的開銷。在記憶體映射中,控制暫存器只是記憶體中的變數,在 C 語言中可以和其他變數一樣進行定址。
  • 第二,對於記憶體映射 I/O ,不需要特殊的保護機制就能夠阻止用戶進程執行 I/O 操作。作業系統需要保證的是禁止把控制暫存器的地址空間放在用戶的虛擬地址中就可以了。
  • 第三,對於記憶體映射 I/O,可以引用記憶體的每一條指令也可以引用控制暫存器,便於引用。

在電腦設計中,幾乎所有的事情都要權衡。記憶體映射 I/O 也是一樣,它也有自己的缺點。首先,大部分電腦現在都會有一些對於記憶體字的快取。快取一個設備控制暫存器的代價是很大的。為了避免這種記憶體映射 I/O 的情況,硬體必須有選擇性的禁用快取,例如,在每個頁面上禁用快取,這個功能為硬體和作業系統增加了額外的複雜性,因此必須選擇性的進行管理。

第二點,如果僅僅只有一個地址空間,那麼所有的記憶體模組(memory modules)和所有的 I/O 設備都必須檢查所有的記憶體引用來推斷出誰來進行響應。

什麼是記憶體模組?在計算中,存儲器模組是其上安裝有存儲器積體電路的印刷電路板。 」

如果電腦是一種單匯流排體系結構的話,如下圖所示

讓每個記憶體模組和 I/O 設備查看每個地址是簡單易行的。

然而,現代個人電腦的趨勢是專用的高速記憶體匯流排,如下圖所示

裝備這一匯流排是為了優化記憶體訪問速度,x86 系統還可以有多種匯流排(記憶體、PCIe、SCSI 和 USB)。如下圖所示

在記憶體映射機器上使用單獨的記憶體匯流排的麻煩之處在於,I/O 設備無法通過記憶體匯流排查看記憶體地址,因此它們無法對其進行響應。此外,必須採取特殊的措施使記憶體映射 I/O 工作在具有多匯流排的系統上。一種可能的方法是首先將全部記憶體引用發送到記憶體,如果記憶體響應失敗,CPU 再嘗試其他匯流排。

第二種設計是在記憶體匯流排上放一個探查設備,放過所有潛在指向所關注的 I/O 設備的地址。此處的問題是,I/O 設備可能無法以記憶體所能達到的速度處理請求。

第三種可能的設計是在記憶體控制器中對地址進行過濾,這種設計與上圖所描述的設計相匹配。這種情況下,記憶體控制器晶片中包含在引導時預裝載的範圍暫存器。這一設計的缺點是需要在引導時判定哪些記憶體地址而不是真正的記憶體地址。因而,每一設計都有支援它和反對它的論據,所以折中和權衡是不可避免的。

直接記憶體訪問

無論一個 CPU 是否具有記憶體映射 I/O,它都需要定址設備控制器以便與它們交換數據。CPU 可以從 I/O 控制器每次請求一個位元組的數據,但是這麼做會浪費 CPU 時間,所以經常會用到一種稱為直接記憶體訪問(Direct Memory Access) 的方案。為了簡化,我們假設 CPU 通過單一的系統匯流排訪問所有的設備和記憶體,該匯流排連接 CPU 、記憶體和 I/O 設備,如下圖所示

現代作業系統實際更為複雜,但是原理是相同的。如果硬體有DMA 控制器,那麼作業系統只能使用 DMA。有時這個控制器會集成到磁碟控制器和其他控制器中,但這種設計需要在每個設備上都裝有一個分離的 DMA 控制器。單個的 DMA 控制器可用於向多個設備傳輸,這種傳輸往往同時進行。

不管 DMA 控制器的物理地址在哪,它都能夠獨立於 CPU 從而訪問系統匯流排,如上圖所示。它包含幾個可由 CPU 讀寫的暫存器,其中包括一個記憶體地址暫存器,位元組計數暫存器和一個或多個控制暫存器。控制暫存器指定要使用的 I/O 埠、傳送方向(從 I/O 設備讀或寫到 I/O 設備)、傳送單位(每次一個位元組或者每次一個字)以及在一次突發傳送中要傳送的位元組數

為了解釋 DMA 的工作原理,我們首先看一下不使用 DMA 該如何進行磁碟讀取。

  • 首先,控制器從磁碟驅動器串列地、一位一位的讀一個塊(一個或多個扇區),直到將整塊資訊放入控制器的內部緩衝區。
  • 讀取校驗和以保證沒有發生讀錯誤。然後控制器會產生一個中斷,當作業系統開始運行時,它會重複的從控制器的緩衝區中一次一個位元組或者一個字地讀取該塊的資訊,並將其存入記憶體中。

DMA 工作原理

當使用 DMA 後,這個過程就會變得不一樣了。首先 CPU 通過設置 DMA 控制器的暫存器對它進行編程,所以 DMA 控制器知道將什麼數據傳送到什麼地方。DMA 控制器還要向磁碟控制器發出一個命令,通知它從磁碟讀數據到其內部的緩衝區並檢驗校驗和。當有效數據位於磁碟控制器的緩衝區中時,DMA 就可以開始了。

DMA 控制器通過在匯流排上發出一個讀請求到磁碟控制器而發起 DMA 傳送,這是第二步。這個讀請求就像其他讀請求一樣,磁碟控制器並不知道或者並不關心它是來自 CPU 還是來自 DMA 控制器。通常情況下,要寫的記憶體地址在匯流排的地址線上,所以當磁碟控制器去匹配下一個字時,它知道將該字寫到什麼地方。寫到記憶體就是另外一個匯流排循環了,這是第三步。當寫操作完成時,磁碟控制器在匯流排上發出一個應答訊號到 DMA 控制器,這是第四步。

然後,DMA 控制器會增加記憶體地址並減少位元組數量。如果位元組數量仍然大於 0 ,就會循環步驟 2 – 步驟 4 ,直到位元組計數變為 0 。此時,DMA 控制器會打斷 CPU 並告訴它傳輸已經完成了。作業系統開始運行時,它不會把磁碟塊拷貝到記憶體中,因為它已經在記憶體中了。

不同 DMA 控制器的複雜程度差別很大。最簡單的 DMA 控制器每次處理一次傳輸,就像上面描述的那樣。更為複雜的情況是一次同時處理很多次傳輸,這樣的控制器內部具有多組暫存器,每個通道一組暫存器。在傳輸每一個字之後,DMA 控制器就決定下一次要為哪個設備提供服務。DMA 控制器可能被設置為使用 輪詢演算法,或者它也有可能具有一個優先順序規劃設計,以便讓某些設備受到比其他設備更多的照顧。假如存在一個明確的方法分辨應答訊號,那麼在同一時間就可以掛起對不同設備控制器的多個請求。

許多匯流排能夠以兩種模式操作:每次一字模式和塊模式。一些 DMA 控制器也能夠使用這兩種方式進行操作。在前一個模式中,DMA 控制器請求傳送一個字並得到這個字。如果 CPU 想要使用匯流排,它必須進行等待。設備可能會偷偷進入並且從 CPU 偷走一個匯流排周期,從而輕微的延遲 CPU。這種機制稱為 周期竊取(cycle stealing)

在塊模式中,DMA 控制器告訴設備獲取匯流排,然後進行一系列的傳輸操作,然後釋放匯流排。這一操作的形式稱為 突發模式(burst mode)。這種模式要比周期竊取更有效因為獲取匯流排佔用了時間,並且一次匯流排獲得的代價是可以同時傳輸多個字。缺點是如果此時進行的是長時間的突發傳送,有可能將 CPU 和其他設備阻塞很長的時間。

在我們討論的這種模型中,有時被稱為 飛越模式(fly-by mode),DMA 控制器會告訴設備控制器把數據直接傳遞到記憶體。一些 DMA 控制器使用的另一種模式是讓設備控制器將字發送給 DMA 控制器,然後 DMA 控制器發出第二條匯流排請求,將字寫到任何可以寫入的地方。採用這種方案,每個傳輸的字都需要一個額外的匯流排周期,但是更加靈活,因為它還可以執行設備到設備的複製,甚至是記憶體到記憶體的複製(通過事先對記憶體進行讀取,然後對記憶體進行寫入)。

大部分的 DMA 控制器使用物理地址進行傳輸。使用物理地址需要作業系統將目標記憶體緩衝區的虛擬地址轉換為物理地址,並將該物理地址寫入 DMA 控制器的地址暫存器中。另一種方案是一些 DMA 控制器將虛擬地址寫入 DMA 控制器中。然後,DMA 控制器必須使用 MMU 才能完成虛擬到物理的轉換。僅當 MMU 是記憶體的一部分而不是 CPU 的一部分時,才可以將虛擬地址放在匯流排上。

重溫中斷

在一台個人電腦體系結構中,中斷結構會如下所示

當一個 I/O 設備完成它的工作後,它就會產生一個中斷(默認作業系統已經開啟中斷),它通過在匯流排上聲明已分配的訊號來實現此目的。主板上的中斷控制器晶片會檢測到這個訊號,然後執行中斷操作。

如果在中斷前沒有其他中斷操作阻塞的話,中斷控制器將立刻對中斷進行處理,如果在中斷前還有其他中斷操作正在執行,或者有其他設備發出級別更高的中斷訊號的話,那麼這個設備將暫時不會處理。在這種情況下,該設備會繼續在匯流排上置起中斷訊號,直到得到 CPU 服務。

為了處理中斷,中斷控制器在地址線上放置一個數字,指定要關注的設備是哪個,並聲明一個訊號以中斷 CPU。中斷訊號導致 CPU 停止當前正在做的工作並且開始做其他事情。地址線上會有一個指向中斷向量表 的索引,用來獲取下一個程式計數器。這個新獲取的程式計數器也就表示著程式將要開始,它會指向程式的開始處。一般情況下,陷阱和中斷從這一點上看使用相同的機制,並且常常共享相同的中斷向量。中斷向量的位置可以硬連線到機器中,也可以位於記憶體中的任何位置,由 CPU 暫存器指向其起點。

中斷服務程式開始運行後,中斷服務程式通過將某個值寫入中斷控制器的 I/O 埠來確認中斷。告訴它中斷控制器可以自由地發出另一個中斷。通過讓 CPU 延遲響應來達到多個中斷同時到達 CPU 涉及到競爭的情況發生。一些老的電腦沒有集中的中斷控制器,通常每個設備請求自己的中斷。

硬體通常在服務程式開始前保存當前資訊。對於不同的 CPU 來說,哪些資訊需要保存以及保存在哪裡差別很大。不管其他的資訊是否保存,程式計數器必須要被保存,這對所有的 CPU 來說都是相同的,以此來恢復中斷的進程。所有可見暫存器和大量內部暫存器也應該被保存。

上面說到硬體應該保存當前資訊,那麼保存在哪裡是個問題,一種選擇是將其放入到內部暫存器中,在需要時作業系統可以讀出這些內部暫存器。這種方法會造成的問題是:一段時間內設備無法響應,直到所有的內部暫存器中存儲的資訊被讀出後,才能恢復運行,以免第二個內部暫存器重寫內部暫存器的狀態。

第二種方式是在堆棧中保存資訊,這也是大部分 CPU 所使用的方式。但是,這種方法也存在問題,因為使用的堆棧不確定,如果使用的是當前堆棧,則它很可能是用戶進程的堆棧。堆棧指針甚至不合法,這樣當硬體試圖在它所指的地址處寫入時,將會導致致命錯誤。如果使用的是內核堆棧,堆棧指針是合法的並且指向一個固定的頁面,這樣的機會可能會更大。然而,切換到內核態需要切換 MMU 上下文,並且可能使高速快取或者 TLB 失效。靜態或動態重新裝載這些東西將增加中斷處理的時間,浪費 CPU 時間。

精確中斷和不精確中斷

另一個問題是:現代 CPU 大量的採用流水線並且有時還採用超標量(內部並行)。在一些老的系統中,每條指令執行完畢後,微程式或硬體將檢查是否存在未完成的中斷。如果存在,那麼程式計數器和 PSW 將被壓入堆棧中開始中斷序列。在中斷程式運行之後,舊的 PSW 和程式計數器將從堆棧中彈出恢復先前的進程。

下面是一個流水線模型

在流水線滿的時候出現一個中斷會發生什麼情況?許多指令正處於不同的執行階段,中斷出現時,程式計數器的值可能無法正確地反應已經執行過的指令和尚未執行的指令的邊界。事實上,許多指令可能部分執行,不同的指令完成的程度或多或少。在這種情況下,程式計數器更有可能反應的是將要被取出並壓入流水線的下一條指令的地址,而不是剛剛被執行單元處理過的指令的地址。

在超標量的設計中,可能更加糟糕

每個指令都可以分解成為微操作,微操作有可能亂序執行,這取決於內部資源(如功能單元和暫存器)的可用性。當中斷髮生時,某些很久以前啟動的指令可能還沒開始執行,而最近執行的指令可能將要馬上完成。在中斷訊號出現時,可能存在許多指令處於不同的完成狀態,它們與程式計數器之間沒有什麼關係。

使機器處於良好狀態的中斷稱為精確中斷(precise interrupt)。這樣的中斷具有四個屬性:

  • PC (程式計數器)保存在一個已知的地方
  • PC 所指向的指令之前所有的指令已經完全執行
  • PC 所指向的指令之後所有的指令都沒有執行
  • PC 所指向的指令的執行狀態是已知的

不滿足以上要求的中斷稱為 不精確中斷(imprecise interrupt),不精確中斷讓人很頭疼。上圖描述了不精確中斷的現象。指令的執行時序和完成度具有不確定性,而且恢復起來也非常麻煩。

相關鏈接

https://pineight.com/ds/block/

https://www.computerhope.com/jargon/i/iodevice.htm

https://en.wikipedia.org/wiki/Memory_module

《現代作業系統》第四版

https://en.wikipedia.org/wiki/Preamble

https://en.wikipedia.org/wiki/Word_(computer_architecture)