計算機最魔幻的事情就是它能感知到你的思想
我們之前的文章提到了操作系統的三個抽象,它們分別是進程、地址空間和文件,除此之外,操作系統還要控制所有的 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++ 中不存在執行 IN 和 OUT指令的方法。調用這樣的過程增加了 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)