說了這麼多次 I/O,但你知道它的原理么

IO 軟體目標

設備獨立性

現在讓我們轉向對 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》列出來的一些方案

  • 非嵌套的中斷處理程式按照順序處理各個中斷,非嵌套的中斷處理程式也是最簡單的中斷處理
  • 嵌套的中斷處理程式會處理多個中斷而無需分配優先順序
  • 可重入的中斷處理程式可使用優先順序處理多個中斷
  • 簡單優先順序中斷處理程式可處理簡單的中斷
  • 標準優先順序中斷處理程式比低優先順序的中斷處理程式在更短的時間能夠處理優先順序更高的中斷
  • 高優先順序 中斷處理程式在短時間能夠處理優先順序更高的任務,並直接進入特定的服務常式。
  • 優先順序分組中斷處理程式能夠處理不同優先順序的中斷任務

下面是一些通用的中斷處理程式的步驟,不同的作業系統實現細節不一樣

  • 保存所有沒有被中斷硬體保存的暫存器
  • 為中斷服務程式設置上下文環境,可能包括設置 TLBMMU 和頁表,如果不太了解這三個概念,請參考另外一篇文章
  • 為中斷服務程式設置棧
  • 對中斷控制器作出響應,如果不存在集中的中斷控制器,則繼續響應中斷
  • 把暫存器從保存它的地方拷貝到進程表中
  • 運行中斷服務程式,它會從發出中斷的設備控制器的暫存器中提取資訊
  • 作業系統會選擇一個合適的進程來運行。如果中斷造成了一些優先順序更高的進程變為就緒態,則選擇運行這些優先順序高的進程
  • 為進程設置 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 軟體和庫過程在用戶空間存在,然後以提供系統調用的方式實現。