
我們之前的文章提到了操作系統的三個抽象,它們分別是進程、地址空間和文件,除此之外,操作系統還要控制所有的 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 和程序計數器將從堆棧中彈出恢復先前的進程。
下面是一個流水線模型

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

每個指令都可以分解成為微操作,微操作有可能亂序執行,這取決於內部資源(如功能單元和寄存器)的可用性。當中斷發生時,某些很久以前啟動的指令可能還沒開始執行,而最近執行的指令可能將要馬上完成。在中斷信號出現時,可能存在許多指令處於不同的完成狀態,它們與程序計數器之間沒有什么關系。
使機器處於良好狀態的中斷稱為精確中斷(precise interrupt)。這樣的中斷具有四個屬性:
- PC (程序計數器)保存在一個已知的地方
- PC 所指向的指令之前所有的指令已經完全執行
- PC 所指向的指令之后所有的指令都沒有執行
- PC 所指向的指令的執行狀態是已知的
不滿足以上要求的中斷稱為 不精確中斷(imprecise interrupt),不精確中斷讓人很頭疼。上圖描述了不精確中斷的現象。指令的執行時序和完成度具有不確定性,而且恢復起來也非常麻煩。
IO 軟件原理
I/O 軟件目標
設備獨立性
現在讓我們轉向對 I/O 軟件的研究,I/O 軟件設計一個很重要的目標就是設備獨立性(device independence)。啥意思呢?這意味着我們能夠編寫訪問任何設備的應用程序,而不用事先指定特定的設備。比如你編寫了一個能夠從設備讀入文件的應用程序,那么這個應用程序可以從硬盤、DVD 或者 USB 進行讀入,不必再為每個設備定制應用程序。這其實就體現了設備獨立性的概念。

再比如說你可以輸入一條下面的指令
sort 輸入 輸出
那么上面這個 輸入 就可以接收來自任意類型的磁盤或者鍵盤,並且 輸出 可以寫入到任意類型的磁盤或者屏幕。

計算機操作系統是這些硬件的媒介,因為不同硬件它們的指令序列不同,所以需要操作系統來做指令間的轉換。
與設備獨立性密切相關的一個指標就是統一命名(uniform naming)。設備的代號應該是一個整數或者是字符串,它們不應該依賴於具體的設備。在 UNIX 中,所有的磁盤都能夠被集成到文件系統中,所以用戶不用記住每個設備的具體名稱,直接記住對應的路徑即可,如果路徑記不住,也可以通過 ls 等指令找到具體的集成位置。舉個例子來說,比如一個 USB 磁盤被掛載到了 /usr/cxuan/backup 下,那么你把文件復制到 /usr/cxuan/backup/device 下,就相當於是把文件復制到了磁盤中,通過這種方式,實現了向任何磁盤寫入文件都相當於是向指定的路徑輸出文件。
錯誤處理
除了設備獨立性外,I/O 軟件實現的第二個重要的目標就是錯誤處理(error handling)。通常情況下來說,錯誤應該交給硬件層面去處理。如果設備控制器發現了讀錯誤的話,它會盡可能的去修復這個錯誤。如果設備控制器處理不了這個問題,那么設備驅動程序應該進行處理,設備驅動程序會再次嘗試讀取操作,很多錯誤都是偶然性的,如果設備驅動程序無法處理這個錯誤,才會把錯誤向上拋到硬件層面(上層)進行處理,很多時候,上層並不需要知道下層是如何解決錯誤的。這就很像項目經理不用把每個決定都告訴老板;程序員不用把每行代碼如何寫告訴項目經理。這種處理方式不夠透明。
同步和異步傳輸
I/O 軟件實現的第三個目標就是 同步(synchronous) 和 異步(asynchronous,即中斷驅動)傳輸。這里先說一下同步和異步是怎么回事吧。
同步傳輸中數據通常以塊或幀的形式發送。發送方和接收方在數據傳輸之前應該具有同步時鍾。而在異步傳輸中,數據通常以字節或者字符的形式發送,異步傳輸則不需要同步時鍾,但是會在傳輸之前向數據添加奇偶校驗位。下面是同步和異步的主要區別

回到正題。大部分物理IO(physical I/O) 是異步的。物理 I/O 中的 CPU 是很聰明的,CPU 傳輸完成后會轉而做其他事情,它和中斷心靈相通,等到中斷發生后,CPU 才會回到傳輸這件事情上來。
I/O 分為兩種:物理I/O 和
邏輯I/O(Logical I/O)。物理 I/O 通常是從磁盤等存儲設備實際獲取數據。邏輯 I/O 是對存儲器(塊,緩沖區)獲取數據。
緩沖
I/O 軟件的最后一個問題是緩沖(buffering)。通常情況下,從一個設備發出的數據不會直接到達最后的設備。其間會經過一系列的校驗、檢查、緩沖等操作才能到達。舉個例子來說,從網絡上發送一個數據包,會經過一系列檢查之后首先到達緩沖區,從而消除緩沖區填滿速率和緩沖區過載。
共享和獨占
I/O 軟件引起的最后一個問題就是共享設備和獨占設備的問題。有些 I/O 設備能夠被許多用戶共同使用。一些設備比如磁盤,讓多個用戶使用一般不會產生什么問題,但是某些設備必須具有獨占性,即只允許單個用戶使用完成后才能讓其他用戶使用。
下面,我們來探討一下如何使用程序來控制 I/O 設備。一共有三種控制 I/O 設備的方法
- 使用程序控制 I/O
- 使用中斷驅動 I/O
- 使用 DMA 驅動 I/O
使用程序控制 I/O
使用程序控制 I/O 又被稱為 可編程I/O,它是指由 CPU 在驅動程序軟件控制下啟動的數據傳輸,來訪問設備上的寄存器或者其他存儲器。CPU 會發出命令,然后等待 I/O 操作的完成。由於 CPU 的速度比 I/O 模塊的速度快很多,因此可編程 I/O 的問題在於,CPU 必須等待很長時間才能等到處理結果。CPU 在等待時會采用輪詢(polling)或者 忙等(busy waiting) 的方式,結果,整個系統的性能被嚴重拉低。可編程 I/O 十分簡單,如果需要等待的時間非常短的話,可編程 I/O 倒是一個很好的方式。一個可編程的 I/O 會經歷如下操作
- CPU 請求 I/O 操作
- I/O 模塊執行響應
- I/O 模塊設置狀態位
- CPU 會定期檢查狀態位
- I/O 不會直接通知 CPU 操作完成
- I/O 也不會中斷 CPU
- CPU 可能會等待或在隨后的過程中返回

使用中斷驅動 I/O
鑒於上面可編程 I/O 的缺陷,我們提出一種改良方案,我們想要在 CPU 等待 I/O 設備的同時,能夠做其他事情,等到 I/O 設備完成后,它就會產生一個中斷,這個中斷會停止當前進程並保存當前的狀態。一個可能的示意圖如下

盡管中斷減輕了 CPU 和 I/O 設備的等待時間的負擔,但是由於還需要在 CPU 和 I/O 模塊之前進行大量的逐字傳輸,因此在大量數據傳輸中效率仍然很低。下面是中斷的基本操作
- CPU 進行讀取操作
- I/O 設備從外圍設備獲取數據,同時 CPU 執行其他操作
- I/O 設備中斷通知 CPU
- CPU 請求數據
- I/O 模塊傳輸數據
所以我們現在着手需要解決的就是 CPU 和 I/O 模塊間數據傳輸的效率問題。
使用 DMA 的 I/O
DMA 的中文名稱是直接內存訪問,它意味着 CPU 授予 I/O 模塊權限在不涉及 CPU 的情況下讀取或寫入內存。也就是 DMA 可以不需要 CPU 的參與。這個過程由稱為 DMA 控制器(DMAC)的芯片管理。由於 DMA 設備可以直接在內存之間傳輸數據,而不是使用 CPU 作為中介,因此可以緩解總線上的擁塞。DMA 通過允許 CPU 執行任務,同時 DMA 系統通過系統和內存總線傳輸數據來提高系統並發性。
I/O 層次結構
I/O 軟件通常組織成四個層次,它們的大致結構如下圖所示

每一層和其上下層都有明確的功能和接口。下面我們采用和計算機網絡相反的套路,即自下而上的了解一下這些程序。
下面是另一幅圖,這幅圖顯示了輸入/輸出軟件系統所有層及其主要功能。

下面我們具體的來探討一下上面的層次結構
中斷處理程序
在計算機系統中,中斷就像女人的脾氣一樣無時無刻都在產生,中斷的出現往往是讓人很不爽的。中斷處理程序又被稱為中斷服務程序 或者是 ISR(Interrupt Service Routines),它是最靠近硬件的一層。中斷處理程序由硬件中斷、軟件中斷或者是軟件異常啟動產生的中斷,用於實現設備驅動程序或受保護的操作模式(例如系統調用)之間的轉換。
中斷處理程序負責處理中斷發生時的所有操作,操作完成后阻塞,然后啟動中斷驅動程序來解決阻塞。通常會有三種通知方式,依賴於不同的具體實現
- 信號量實現中:在信號量上使用
up進行通知; - 管程實現:對管程中的條件變量執行
signal操作 - 還有一些情況是發送一些消息
不管哪種方式都是為了讓阻塞的中斷處理程序恢復運行。
中斷處理方案有很多種,下面是 《ARM System Developer’s Guide
Designing and Optimizing System Software》列出來的一些方案
非嵌套的中斷處理程序按照順序處理各個中斷,非嵌套的中斷處理程序也是最簡單的中斷處理嵌套的中斷處理程序會處理多個中斷而無需分配優先級可重入的中斷處理程序可使用優先級處理多個中斷簡單優先級中斷處理程序可處理簡單的中斷標准優先級中斷處理程序比低優先級的中斷處理程序在更短的時間能夠處理優先級更高的中斷高優先級中斷處理程序在短時間能夠處理優先級更高的任務,並直接進入特定的服務例程。優先級分組中斷處理程序能夠處理不同優先級的中斷任務
下面是一些通用的中斷處理程序的步驟,不同的操作系統實現細節不一樣
- 保存所有沒有被中斷硬件保存的寄存器
- 為中斷服務程序設置上下文環境,可能包括設置
TLB、MMU和頁表,如果不太了解這三個概念,請參考另外一篇文章 - 為中斷服務程序設置棧
- 對中斷控制器作出響應,如果不存在集中的中斷控制器,則繼續響應中斷
- 把寄存器從保存它的地方拷貝到進程表中
- 運行中斷服務程序,它會從發出中斷的設備控制器的寄存器中提取信息
- 操作系統會選擇一個合適的進程來運行。如果中斷造成了一些優先級更高的進程變為就緒態,則選擇運行這些優先級高的進程
- 為進程設置 MMU 上下文,可能也會需要 TLB,根據實際情況決定
- 加載進程的寄存器,包括 PSW 寄存器
- 開始運行新的進程
上面我們羅列了一些大致的中斷步驟,不同性質的操作系統和中斷處理程序能夠處理的中斷步驟和細節也不盡相同,下面是一個嵌套中斷的具體運行步驟

設備驅動程序
在上面的文章中我們知道了設備控制器所做的工作。我們知道每個控制器其內部都會有寄存器用來和設備進行溝通,發送指令,讀取設備的狀態等。
因此,每個連接到計算機的 I/O 設備都需要有某些特定設備的代碼對其進行控制,例如鼠標控制器需要從鼠標接受指令,告訴下一步應該移動到哪里,鍵盤控制器需要知道哪個按鍵被按下等。這些提供 I/O 設備到設備控制器轉換的過程的代碼稱為 設備驅動程序(Device driver)。
為了能夠訪問設備的硬件,實際上也就意味着,設備驅動程序通常是操作系統內核的一部分,至少現在的體系結構是這樣的。但是也可以構造用戶空間的設備驅動程序,通過系統調用來完成讀寫操作。這樣就避免了一個問題,有問題的驅動程序會干擾內核,從而造成崩潰。所以,在用戶控件實現設備驅動程序是構造系統穩定性一個非常有用的措施。MINIX 3 就是這么做的。下面是 MINI 3 的調用過程

然而,大多數桌面操作系統要求驅動程序必須運行在內核中。
操作系統通常會將驅動程序歸為 字符設備 和 塊設備,我們上面也介紹過了

在 UNIX 系統中,操作系統是一個二進制程序,包含需要編譯到其內部的所有驅動程序,如果你要對 UNIX 添加一個新設備,需要重新編譯內核,將新的驅動程序裝到二進制程序中。
然而隨着大多數個人計算機的出現,由於 I/O 設備的廣泛應用,上面這種靜態編譯的方式不再有效,因此,從 MS-DOS 開始,操作系統轉向驅動程序在執行期間動態的裝載到系統中。
設備驅動程序具有很多功能,比如接受讀寫請求,對設備進行初始化、管理電源和日志、對輸入參數進行有效性檢查等。
設備驅動程序接受到讀寫請求后,會檢查當前設備是否在使用,如果設備在使用,請求被排入隊列中,等待后續的處理。如果此時設備是空閑的,驅動程序會檢查硬件以了解請求是否能夠被處理。在傳輸開始前,會啟動設備或者馬達。等待設備就緒完成,再進行實際的控制。控制設備就是對設備發出指令。
發出命令后,設備控制器便開始將它們寫入控制器的設備寄存器。在將每個命令寫入控制器后,會檢查控制器是否接受了這條命令並准備接受下一個命令。一般控制設備會發出一系列的指令,這稱為指令序列,設備控制器會依次檢查每個命令是否被接受,下一條指令是否能夠被接收,直到所有的序列發出為止。

發出指令后,一般會有兩種可能出現的情況。在大多數情況下,設備驅動程序會進行等待直到控制器完成它的事情。這里需要了解一下設備控制器的概念
設備控制器的主要主責是控制一個或多個 I/O 設備,以實現 I/O 設備和計算機之間的數據交換。
設備控制器接收從 CPU 發送過來的指令,繼而達到控制硬件的目的
設備控制器是一個可編址的設備,當它僅控制一個設備時,它只有一個唯一的設備地址;如果設備控制器控制多個可連接設備時,則應含有多個設備地址,並使每一個設備地址對應一個設備。
設備控制器主要分為兩種:字符設備和塊設備
設備控制器的主要功能有下面這些
-
接收和識別命令:設備控制器可以接受來自 CPU 的指令,並進行識別。設備控制器內部也會有寄存器,用來存放指令和參數
-
進行數據交換:CPU、控制器和設備之間會進行數據的交換,CPU 通過總線把指令發送給控制器,或從控制器中並行地讀出數據;控制器將數據寫入指定設備。
-
地址識別:每個硬件設備都有自己的地址,設備控制器能夠識別這些不同的地址,來達到控制硬件的目的,此外,為使 CPU 能向寄存器中寫入或者讀取數據,這些寄存器都應具有唯一的地址。
-
差錯檢測:設備控制器還具有對設備傳遞過來的數據進行檢測的功能。
在這種情況下,設備控制器會阻塞,直到中斷來解除阻塞狀態。還有一種情況是操作是可以無延遲的完成,所以驅動程序不需要阻塞。在第一種情況下,操作系統可能被中斷喚醒;第二種情況下操作系統不會被休眠。
設備驅動程序必須是可重入的,因為設備驅動程序會阻塞和喚醒然后再次阻塞。驅動程序不允許進行系統調用,但是它們通常需要與內核的其余部分進行交互。
與設備無關的 I/O 軟件
I/O 軟件有兩種,一種是我們上面介紹過的基於特定設備的,還有一種是設備無關性的,設備無關性也就是不需要特定的設備。設備驅動程序與設備無關的軟件之間的界限取決於具體的系統。下面顯示的功能由設備無關的軟件實現

與設備無關的軟件的基本功能是對所有設備執行公共的 I/O 功能,並且向用戶層軟件提供一個統一的接口。
緩沖
無論是對於塊設備還是字符設備來說,緩沖都是一個非常重要的考量標准。下面是從 ADSL(調制解調器) 讀取數據的過程,調制解調器是我們用來聯網的設備。
用戶程序調用 read 系統調用阻塞用戶進程,等待字符的到來,這是對到來的字符進行處理的一種方式。每一個到來的字符都會造成中斷。中斷服務程序會給用戶進程提供字符,並解除阻塞。將字符提供給用戶程序后,進程會去讀取其他字符並繼續阻塞,這種模型如下

這一種方案是沒有緩沖區的存在,因為用戶進程如果讀不到數據會阻塞,直到讀到數據為止,這種情況效率比較低,而且阻塞式的方式,會直接阻止用戶進程做其他事情,這對用戶來說是不能接受的。還有一種情況就是每次用戶進程都會重啟,對於每個字符的到來都會重啟用戶進程,這種效率會嚴重降低,所以無緩沖區的軟件不是一個很好的設計。
作為一個改良點,我們可以嘗試在用戶空間中使用一個能讀取 n 個字節緩沖區來讀取 n 個字符。這樣的話,中斷服務程序會把字符放到緩沖區中直到緩沖區變滿為止,然后再去喚醒用戶進程。這種方案要比上面的方案改良很多。

但是這種方案也存在問題,當字符到來時,如果緩沖區被調出內存會出現什么問題?解決方案是把緩沖區鎖定在內存中,但是這種方案也會出現問題,如果少量的緩沖區被鎖定還好,如果大量的緩沖區被鎖定在內存中,那么可以換進換出的頁面就會收縮,造成系統性能的下降。
一種解決方案是在內核中內部創建一塊緩沖區,讓中斷服務程序將字符放在內核內部的緩沖區中。

當內核中的緩沖區要滿的時候,會將用戶空間中的頁面調入內存,然后將內核空間的緩沖區復制到用戶空間的緩沖區中,這種方案也面臨一個問題就是假如用戶空間的頁面被換入內存,此時內核空間的緩沖區已滿,這時候仍有新的字符到來,這個時候會怎么辦?因為緩沖區滿了,沒有空間來存儲新的字符了。
一種非常簡單的方式就是再設置一個緩沖區就行了,在第一個緩沖區填滿后,在緩沖區清空前,使用第二個緩沖區,這種解決方式如下

當第二個緩沖區也滿了的時候,它也會把數據復制到用戶空間中,然后第一個緩沖區用於接受新的字符。這種具有兩個緩沖區的設計被稱為 雙緩沖(double buffering)。
還有一種緩沖形式是 循環緩沖(circular buffer)。它由一個內存區域和兩個指針組成。一個指針指向下一個空閑字,新的數據可以放在此處。另外一個指針指向緩沖區中尚未刪除數據的第一個字。在許多情況下,硬件會在添加新的數據時,移動第一個指針;而操作系統會在刪除和處理無用數據時會移動第二個指針。兩個指針到達頂部時就回到底部重新開始。
緩沖區對輸出來說也很重要。對輸出的描述和輸入相似
緩沖技術應用廣泛,但它也有缺點。如果數據被緩沖次數太多,會影響性能。考慮例如如下這種情況,

數據經過用戶進程 -> 內核空間 -> 網絡控制器,這里的網絡控制器應該就相當於是 socket 緩沖區,然后發送到網絡上,再到接收方的網絡控制器 -> 接收方的內核緩沖 -> 接收方的用戶緩沖,一條數據包被緩存了太多次,很容易降低性能。
錯誤處理
在 I/O 中,出錯是一種再正常不過的情況了。當出錯發生時,操作系統必須盡可能處理這些錯誤。有一些錯誤是只有特定的設備才能處理,有一些是由框架進行處理,這些錯誤和特定的設備無關。
I/O 錯誤的一類是程序員編程錯誤,比如還沒有打開文件前就讀流,或者不關閉流導致內存溢出等等。這類問題由程序員處理;另外一類是實際的 I/O 錯誤,例如向一個磁盤壞塊寫入數據,無論怎么寫都寫入不了。這類問題由驅動程序處理,驅動程序處理不了交給硬件處理,這個我們上面也說過。
設備驅動程序統一接口
我們在操作系統概述中說到,操作系統一個非常重要的功能就是屏蔽了硬件和軟件的差異性,為硬件和軟件提供了統一的標准,這個標准還體現在為設備驅動程序提供統一的接口,因為不同的硬件和廠商編寫的設備驅動程序不同,所以如果為每個驅動程序都單獨提供接口的話,這樣沒法搞,所以必須統一。
分配和釋放
一些設備例如打印機,它只能由一個進程來使用,這就需要操作系統根據實際情況判斷是否能夠對設備的請求進行檢查,判斷是否能夠接受其他請求,一種比較簡單直接的方式是在特殊文件上執行 open操作。如果設備不可用,那么直接 open 會導致失敗。還有一種方式是不直接導致失敗,而是讓其阻塞,等到另外一個進程釋放資源后,在進行 open 打開操作。這種方式就把選擇權交給了用戶,由用戶判斷是否應該等待。
注意:阻塞的實現有多種方式,有阻塞隊列等
設備無關的塊
不同的磁盤會具有不同的扇區大小,但是軟件不會關心扇區大小,只管存儲就是了。一些字符設備可以一次一個字節的交付數據,而其他的設備則以較大的單位交付數據,這些差異也可以隱藏起來。
用戶空間的 I/O 軟件
雖然大部分 I/O 軟件都在內核結構中,但是還有一些在用戶空間實現的 I/O 軟件,凡事沒有絕對。一些 I/O 軟件和庫過程在用戶空間存在,然后以提供系統調用的方式實現。
盤
盤可以說是硬件里面比較簡單的構造了,同時也是最重要的。下面我們從盤談起,聊聊它的物理構造
盤硬件
盤會有很多種類型。其中最簡單的構造就是磁盤(magnetic hard disks), 也被稱為 hard disk,HDD等。磁盤通常與安裝在磁臂上的磁頭配對,磁頭可將數據讀取或者將數據寫入磁盤,因此磁盤的讀寫速度都同樣快。在磁盤中,數據是隨機訪問的,這也就說明可以通過任意的順序來存儲和檢索單個數據塊,所以你可以在任意位置放置磁盤來讓磁頭讀取,磁盤是一種非易失性的設備,即使斷電也能永久保留。
在計算機發展早期一般是用光盤來存儲數據的,然而隨着固態硬盤的流行,固態硬盤不包含運動部件的特點,成為現在計算機的首選存儲方式。
磁盤
為了組織和檢索數據,會將磁盤組織成特定的結構,這些特定的結構就是磁道、扇區和柱面

每一個磁盤都是由無數個同心圓組成,這些同心圓就好像樹的年輪一樣

部分樹的年輪照片都要付費下載了,不敢直接白嫖,闊怕闊怕。
磁盤被組織成柱面形式,每個盤用軸相連,每一個柱面包含若干磁道,每個磁道由若干扇區組成。軟盤上大約每個磁道有 8 - 32 個扇區,硬盤上每條磁道上扇區的數量可達幾百個,磁頭大約是 1 - 16 個。
對於磁盤驅動程序來說,一個非常重要的特性就是控制器是否能夠同時控制兩個或者多個驅動器進行磁道尋址,這就是重疊尋道(overlapped seek)。對於控制器來說,它能夠控制一個磁盤驅動程序完成尋道操作,同時讓其他驅動程序等待尋道結束。控制器也可以在一個驅動程序上進行讀寫草哦做,與此同時讓另外的驅動器進行尋道操作,但是軟盤控制器不能在兩個驅動器上進行讀寫操作。
RAID
RAID 稱為 磁盤冗余陣列,簡稱 磁盤陣列。利用虛擬化技術把多個硬盤結合在一起,成為一個或多個磁盤陣列組,目的是提升性能或數據冗余。
RAID 有不同的級別
- RAID 0 - 無容錯的條帶化磁盤陣列
- RAID 1 - 鏡像和雙工
- RAID 2 - 內存式糾錯碼
- RAID 3 - 比特交錯奇偶校驗
- RAID 4 - 塊交錯奇偶校驗
- RAID 5 - 塊交錯分布式奇偶校驗
- RAID 6 - P + Q冗余
磁盤格式化
磁盤由一堆鋁的、合金或玻璃的盤片組成,磁盤剛被創建出來后,沒有任何信息。磁盤在使用前必須經過低級格式化(low-levvel format),下面是一個扇區的格式

前導碼相當於是標示扇區的開始位置,通常以位模式開始,前導碼還包括柱面號、扇區號等一些其他信息。緊隨前導碼后面的是數據區,數據部分的大小由低級格式化程序來確定。大部分磁盤使用 512 字節的扇區。數據區后面是 ECC,ECC 的全稱是 error correction code ,數據糾錯碼,它與普通的錯誤檢測不同,ECC 還可以用於恢復讀錯誤。ECC 階段的大小由不同的磁盤制造商實現。ECC 大小的設計標准取決於設計者願意犧牲多少磁盤空間來提高可靠性,以及程序可以處理的 ECC 的復雜程度。通常情況下 ECC 是 16 位,除此之外,硬盤一般具有一定數量的備用扇區,用於替換制造缺陷的扇區。
低級格式化后的每個 0 扇區的位置都和前一個磁道存在偏移,如下圖所示

這種方式又被稱為 柱面斜進(cylinder skew),之所以采用這種方式是為了提高程序的運行性能。可以這樣想,磁盤在轉動的過程中會經由磁頭來讀取扇區信息,在讀取內側一圈扇區數據后,磁頭會進行向外側磁道的尋址操作,尋址操作的同時磁盤在繼續轉動,如果不采用這種方式,可能剛好磁頭尋址到外側,0 號扇區已經轉過了磁頭,所以需要旋轉一圈才能等到它繼續讀取,通過柱面斜進的方式可以消除這一問題。
柱面斜進量取決於驅動器的幾何規格。柱面斜進量就是兩個相鄰同心圓 0 號扇區的差異量。如下圖所示

這里需要注意一點,不只有柱面存在斜進,磁頭也會存在斜進(head skew),但是磁頭斜進比較小。
磁盤格式化會減少磁盤容量,減少的磁盤容量都會由前導碼、扇區間間隙和 ECC 的大小以及保留的備用扇區數量。
在磁盤使用前,還需要經過最后一道工序,那就是對每個分區分別執行一次高級格式化(high-level format),這一操作要設置一個引導塊、空閑存儲管理(采用位圖或者是空閑列表)、根目錄和空文件系統。這一步操作會把碼放在分區表項中,告訴分區使用的是哪種文件系統,因為許多操作系統支持多個兼容的文件系統。在這一步之后,系統就可以進行引導過程。
當電源通電后,BIOS 首先運行,它會讀取主引導記錄並跳轉到主引導記錄中。然后引導程序會檢查以了解哪個分區是處於活動的。然后,它從該分區讀取啟動扇區(boot sector)並運行它。啟動扇區包含一個小程序來加載一個更大一點的引導器來搜索文件系統以找到系統內核(system kernel),然后程序被轉載進入內存並執行。
這里說下什么是引導扇區:引導扇區是磁盤或者存儲設備的保留扇區,其中包含用於完成計算機或磁盤引導過程所必要的數據或者代碼。
引導扇區存儲引導記錄數據,這些數據用於在計算機啟動時提供指令。有兩種不同類型的引導扇區
- Master boot record 稱為主引導扇區
- Volume boot record 卷啟動記錄
對於分區磁盤,引導扇區由主引導記錄組成;
非分區磁盤由卷啟動記錄組成。
磁盤臂調度算法
下面我們來探討一下關於影響磁盤讀寫的算法,一般情況下,影響磁盤快讀寫的時間由下面幾個因素決定
- 尋道時間 - 尋道時間指的就是將磁盤臂移動到需要讀取磁盤塊上的時間
- 旋轉延遲 - 等待合適的扇區旋轉到磁頭下所需的時間
- 實際數據的讀取或者寫入時間
這三種時間參數也是磁盤尋道的過程。一般情況下,尋道時間對總時間的影響最大,所以,有效的降低尋道時間能夠提高磁盤的讀取速度。
如果磁盤驅動程序每次接收一個請求並按照接收順序完成請求,這種處理方式也就是 先來先服務(First-Come, First-served, FCFS) ,這種方式很難優化尋道時間。因為每次都會按照順序處理,不管順序如何,有可能這次讀完后需要等待一個磁盤旋轉一周才能繼續讀取,而其他柱面能夠馬上進行讀取,這種情況下每次請求也會排隊。
通常情況下,磁盤在進行尋道時,其他進程會產生其他的磁盤請求。磁盤驅動程序會維護一張表,表中會記錄着柱面號當作索引,每個柱面未完成的請求會形成鏈表,鏈表頭存放在表的相應表項中。
一種對先來先服務的算法改良的方案是使用 最短路徑優先(SSF) 算法,下面描述了這個算法。
假如我們在對磁道 6 號進行尋址時,同時發生了對 11 , 2 , 4, 14, 8, 15, 3 的請求,如果采用先來先服務的原則,如下圖所示

我們可以計算一下磁盤臂所跨越的磁盤數量為 5 + 9 + 2 + 10 + 6 + 7 + 12 = 51,相當於是跨越了 51 次盤面,如果使用最短路徑優先,我們來計算一下跨越的盤面

跨越的磁盤數量為 4 + 1 + 1 + 4 + 3 + 3 + 1 = 17 ,相比 51 足足省了兩倍的時間。
但是,最短路徑優先的算法也不是完美無缺的,這種算法照樣存在問題,那就是優先級 問題,
這里有一個原型可以參考就是我們日常生活中的電梯,電梯使用一種電梯算法(elevator algorithm) 來進行調度,從而滿足協調效率和公平性這兩個相互沖突的目標。電梯一般會保持向一個方向移動,直到在那個方向上沒有請求為止,然后改變方向。
電梯算法需要維護一個二進制位,也就是當前的方向位:UP(向上)或者是 DOWN(向下)。當一個請求處理完成后,磁盤或電梯的驅動程序會檢查該位,如果此位是 UP 位,磁盤臂或者電梯倉移到下一個更高跌未完成的請求。如果高位沒有未完成的請求,則取相反方向。當方向位是 DOWN 時,同時存在一個低位的請求,磁盤臂會轉向該點。如果不存在的話,那么它只是停止並等待。
我們舉個例子來描述一下電梯算法,比如各個柱面得到服務的順序是 4,7,10,14,9,6,3,1 ,那么它的流程圖如下

所以電梯算法需要跨越的盤面數量是 3 + 3 + 4 + 5 + 3 + 3 + 1 = 22
電梯算法通常情況下不如 SSF 算法。
一些磁盤控制器為軟件提供了一種檢查磁頭下方當前扇區號的方法,使用這樣的控制器,能夠進行另一種優化。如果對一個相同的柱面有兩個或者多個請求正等待處理,驅動程序可以發出請求讀寫下一次要通過磁頭的扇區。
這里需要注意一點,當一個柱面有多條磁道時,相繼的請求可能針對不同的磁道,這種選擇沒有代價,因為選擇磁頭不需要移動磁盤臂也沒有旋轉延遲。
對於磁盤來說,最影響性能的就是尋道時間和旋轉延遲,所以一次只讀取一個或兩個扇區的效率是非常低的。出於這個原因,許多磁盤控制器總是讀出多個扇區並進行高速緩存,即使只請求一個扇區時也是這樣。一般情況下讀取一個扇區的同時會讀取該扇區所在的磁道或者是所有剩余的扇區被讀出,讀出扇區的數量取決於控制器的高速緩存中有多少可用的空間。
磁盤控制器的高速緩存和操作系統的高速緩存有一些不同,磁盤控制器的高速緩存用於緩存沒有實際被請求的塊,而操作系統維護的高速緩存由顯示地讀出的塊組成,並且操作系統會認為這些塊在近期仍然會頻繁使用。
當同一個控制器上有多個驅動器時,操作系統應該為每個驅動器都單獨的維護一個未完成的請求表。一旦有某個驅動器閑置時,就應該發出一個尋道請求來將磁盤臂移到下一個被請求的柱面。如果下一個尋道請求到來時恰好沒有磁盤臂處於正確的位置,那么驅動程序會在剛剛完成傳輸的驅動器上發出一個新的尋道命令並等待,等待下一次中斷到來時檢查哪個驅動器處於閑置狀態。
錯誤處理
磁盤在制造的過程中可能會有瑕疵,如果瑕疵比較小,比如只有幾位,那么使用壞扇區並且每次只是讓 ECC 糾正錯誤是可行的,如果瑕疵較大,那么錯誤就不可能被掩蓋。
一般壞塊有兩種處理辦法,一種是在控制器中進行處理;一種是在操作系統層面進行處理。
這兩種方法經常替換使用,比如一個具有 30 個數據扇區和兩個備用扇區的磁盤,其中扇區 4 是有瑕疵的。

控制器能做的事情就是將備用扇區之一重新映射。

還有一種處理方式是將所有的扇區都向上移動一個扇區

上面這這兩種情況下控制器都必須知道哪個扇區,可以通過內部的表來跟蹤這一信息,或者通過重寫前導碼來給出重新映射的扇區號。如果是重寫前導碼,那么涉及移動的方式必須重寫后面所有的前導碼,但是最終會提供良好的性能。
穩定存儲器
磁盤經常會出現錯誤,導致好的扇區會變成壞扇區,驅動程序也有可能掛掉。RAID 可以對扇區出錯或者是驅動器崩潰提出保護,然而 RAID 卻不能對壞數據中的寫錯誤提供保護,也不能對寫操作期間的崩潰提供保護,這樣就會破壞原始數據。
我們期望磁盤能夠准確無誤的工作,但是事實情況是不可能的,但是我們能夠知道的是,一個磁盤子系統具有如下特性:當一個寫命令發給它時,磁盤要么正確地寫數據,要么什么也不做,讓現有的數據完整無誤的保留。這樣的系統稱為 穩定存儲器(stable storage)。 穩定存儲器的目標就是不惜一切代價保證磁盤的一致性。
穩定存儲器使用兩個一對相同的磁盤,對應的塊一同工作形成一個無差別的塊。穩定存儲器為了實現這個目的,定義了下面三種操作:
穩定寫(stable write)穩定讀(stable read)崩潰恢復(crash recovery)
穩定寫指的就是首先將塊寫到比如驅動器 1 上,然后將其讀回來驗證寫入的是否正確,如果不正確,那么就會再次嘗試寫入和讀取,一直到能夠驗證寫入正確為止。如果塊都寫完了也沒有驗證正確,就會換塊繼續寫入和讀取,直到正確為止。無論嘗試使用多少個備用塊,都是在對你驅動器 1 寫入成功之后,才會對驅動器 2 進行寫入和讀取。這樣我們相當於是對兩個驅動器進行寫入。
穩定讀指的就是首先從驅動器 1 上進行讀取,如果讀取操作會產生錯誤的 ECC,則再次嘗試讀取,如果所有的讀取操作都會給出錯誤的 ECC,那么會從驅動器 2 上進行讀取。這樣我們相當於是對兩個驅動器進行讀取。
崩潰恢復指的是崩潰之后,恢復程序掃描兩個磁盤,比較對應的塊。如果一對塊都是好的並且是相同的,就不會觸發任何機制;如果其中一個塊觸發了 ECC 錯誤,這時候就需要使用好塊來覆蓋壞塊。
如果 CPU 沒有崩潰的話,那么這種方式是可行的,因為穩定寫總是對每個塊寫下兩個有效的副本,並且假設自發的錯誤不會再相同的時刻發生在兩個對應的塊上。如果在穩定寫期間出現 CPU 崩潰會怎么樣?這就取決於崩潰發生的精確時間,有五種情況,下面來說一下
- 第一種情況是崩潰發生在寫入之前,在恢復的時候就什么都不需要修改,舊的值也會繼續存在。

- 第二種情況是 CPU 崩潰發生在寫入驅動器 1 的時候,崩潰導致塊內容被破壞,然而恢復程序能夠檢測出這一種錯誤,並且從驅動器 2 恢復驅動器 1 上的塊。

- 第三種情況是崩潰發生在磁盤驅動器 1 之后但是還沒有寫驅動器 2 之前,這種情況下由於磁盤 1 已經寫入成功

- 第四種情況是崩潰發生在磁盤驅動 1 寫入后在磁盤驅動 2 寫入時,恢復期間會用好的塊替換壞的塊,兩個塊的最終值都是最新的

- 最后一種情況就是崩潰發生在兩個磁盤驅動寫入后,這種情況下不會發生任何問題

這種模式下進行任何優化和改進都是可行的,但是代價高昂,一種改進是在穩定寫期間監控被寫入的塊,這樣在崩潰后進行檢驗的塊只有一個。有一種 非易失性 RAM 能夠在崩潰之后保留數據,但是這種方式並不推薦使用。
時鍾
時鍾(Clocks) 也被稱為定時器(timers),時鍾/定時器對任何程序系統來說都是必不可少的。時鍾負責維護時間、防止一個進程長期占用 CPU 時間等其他功能。時鍾軟件(clock software) 也是一種設備驅動的方式。下面我們就來對時鍾進行介紹,一般都是先討論硬件再介紹軟件,采用由下到上的方式,也是告訴你,底層是最重要的。
時鍾硬件
在計算機中有兩種類型的時鍾,這些時鍾與現實生活中使用的時鍾完全不一樣。
- 比較簡單的一種時鍾被連接到 110 V 或 220 V 的電源線上,這樣每個
電壓周期會產生一個中斷,大概是 50 - 60 HZ。這些時鍾過去一直占據支配地位。 - 另外的一種時鍾由晶體振盪器、計數器和寄存器組成,示意圖如下所示

這種時鍾稱為可編程時鍾 ,可編程時鍾有兩種模式,一種是 一鍵式(one-shot mode),當時鍾啟動時,會把存儲器中的值復制到計數器中,然后,每次晶體的振盪器的脈沖都會使計數器 -1。當計數器變為 0 時,會產生一個中斷,並停止工作,直到軟件再一次顯示啟動。還有一種模式時 方波(square-wave mode) 模式,在這種模式下,當計數器變為 0 並產生中斷后,存儲寄存器的值會自動復制到計數器中,這種周期性的中斷稱為一個時鍾周期。
時鍾軟件
時鍾硬件所做的工作只是根據已知的時間間隔產生中斷,而其他的工作都是由時鍾軟件來完成,一般操作系統的不同,時鍾軟件的具體實現也不同,但是一般都會包括以下這幾點
- 維護一天的時間
- 阻止進程運行的時間超過其指定時間
- 統計 CPU 的使用情況
- 處理用戶進程的警告系統調用
- 為系統各個部分提供看門狗定時器
- 完成概要剖析,監視和信息收集
軟定時器
時鍾軟件也被稱為可編程時鍾,可以設置它以程序需要的任何速率引發中斷。時鍾軟件觸發的中斷是一種硬中斷,但是某些應用程序對於硬中斷來說是不可接受的。
這時候就需要一種軟定時器(soft timer) 避免了中斷,無論何時當內核因為某種原因呢在運行時,它返回用戶態之前都會檢查時鍾來了解軟定時器是否到期。如果軟定時器到期,則執行被調度的事件也無需切換到內核態,因為本身已經處於內核態中。這種方式避免了頻繁的內核態和用戶態之前的切換,提高了程序運行效率。
軟定時器因為不同的原因切換進入內核態的速率不同,原因主要有
- 系統調用
- TLB 未命中
- 缺頁異常
- I/O 中斷
- CPU 變得空閑
我發起了一個成為最好的 bestJavaer 開源項目,地址是 https://github.com/crisxuan/bestJavaer,歡迎各位 star。


