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》列出來的一些方案
非巢狀
的中斷處理程式按照順序處理各個中斷,非巢狀的中斷處理程式也是最簡單的中斷處理巢狀
的中斷處理程式會處理多箇中斷而無需分配優先順序可重入
的中斷處理程式可使用優先順序處理多箇中斷簡單優先順序
中斷處理程式可處理簡單的中斷標準優先順序
中斷處理程式比低優先順序的中斷處理程式在更短的時間能夠處理優先順序更高的中斷高優先順序
中斷處理程式在短時間能夠處理優先順序更高的任務,並直接進入特定的服務例程。優先順序分組
中斷處理程式能夠處理不同優先順序的中斷任務
下面是一些通用的中斷處理程式的步驟,不同的作業系統實現細節不一樣
- 儲存所有沒有被中斷硬體儲存的暫存器
- 為中斷服務程式設定上下文環境,可能包括設定
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 軟體和庫過程在使用者空間存在,然後以提供系統呼叫的方式實現。