1

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 系統通過系統和內存總線傳輸數據來提高系統併發性。

2

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 軟件和庫過程在用戶空間存在,然後以提供系統調用的方式實現。

相關文章