在看文章前可以先看下這個,

吳海波:專欄的序。

先有個大概的認識會對閱讀有所幫助。

在介紹後續章節內容之前,我們介紹下I/O設備這個概念以及OS如何和這些設備進行交互。I/O對於計算機系統來說是非常重要的,想像下,如果一個程序沒有輸入或者輸出,那麼會是什麼樣的場景(你永遠不能操作什麼也無法得到什麼)?。。。。所以為了讓計算機系統更有吸引力,就必須要同時存在輸入和輸出,所以我們遇到的問題是:

如何將I/O設備集成到計算機系統中?

通用的原理是什麼?

我們怎麼讓這些設備高效的運行?

在開始討論前,先看下下面這個「經典」的計算機系統的圖示。

圖中cpu和主存通過內存匯流排連接。一些設備通過I/O匯流排和系統進行連接,在大多數現代操作系統中是PCI(或是其它的衍生品);包括圖像處理以及其他一些需要高傳輸速率的設備。其他的一些比較慢的設備,比如SCISI,SATA,USB是通過「外圍匯流排「和系統連接的。通常硬碟,滑鼠和鍵盤就是屬於慢速的設備。

你可能會想:為什麼我們需要一個像這樣的層次結構? 簡單地說:客觀世界的限制和成本。 比如公交車越快,公交線路必須越短; 因此,高性能的內存匯流排沒有空間插入很多的設備。 另外,高性能的公共汽車也會非常昂貴。 因此,系統設計師採用這種分層方法,讓其中需要高性能的組件(如顯卡)更靠近CPU,低性能的設備則相對較遠。放置磁碟和其他慢速設備在外圍匯流排上的好處是多方面的; 尤其是可以在其上放置大量設備(這裡的想法其實和緩存的概念是相似的)。

當然,現代系統越來越多地使用專用晶元組和更快的點對點互連,以提高性能。 圖36.2顯示了英特爾Z270晶元組的大致設計圖。在頂部,CPU緊密地連接到內存系統,而且還具有與顯卡的高性能連接。

CPU通過英特爾專有的DMI(Direct Media Interface)連接I/O晶元,外圍的設備通過不同種類的內部連接到I/O晶元。 在右側,一個或多個硬碟驅動器通過eSATA介面連接到系統; ATA(AT Attachment,用來連接IBM PC AT類型機器的介面),然後是SATA(Serial ATA),現在eSATA(external SATA),這些介面的更新迭代代表著過去幾十年的存儲介面的進化,每一步都提高性能以跟上現代存儲設備的步伐。I / O晶元下面是許多USB(通用串列匯流排)連接,可以連接鍵盤和滑鼠到電腦。 在許多現代系統中,USB用於低速的設備。

最後,在左側,可以通過PCIe(Peripheral Component Interconnect Express)連接其他更高性能的設備到系統。 在此圖中,網路通過PCIe介面連接到系統; 更高性能存儲設備(如NVMe持久存儲設備)通常也在這裡連接。

現在讓我們看一個典型設備(不是真實設備),並使用該設備,來推動我們對於設備高速連接所需的一些原理的理解。從圖36.3,我們可以看到設備有兩個重要組件。首先是硬體層面呈現給外部的介面。就像軟體一樣,硬體必須提供允許系統軟體進行操作的介面。因此,所有設備都有一些指定的典型交互介面和交互協議。

設備的第二部分是其內部結構。這一部分是每個設備自己的特定實現,這個部分負責實現設備呈現給系統的抽象。非常簡單的設備將有一個或幾個硬體晶元來實現他們的功能;更複雜的設備可能包括一個簡單的CPU,一些通用的內存和其他完成特定功能的晶元。例如,現代RAID控制器可能包含數十萬行固件(即硬體設備內的軟體)以實現其功能。

在上圖中,(簡化)設備介面由三個寄存器組成:狀態寄存器,可以讀取以查看設備的當前狀態; 命令寄存器,告訴設備執行某項任務; 和數據寄存器,用於將數據傳遞給設備,或從設備獲取數據。

通過讀寫這些寄存器,操作系統可以控制設備行為。

下面是描述操作系統可能與設備進行的典型交互。 協議如下:

While (STATUS == BUSY)//當設備處於忙的狀態的時候,cpu循環等待
; // wait until device is not busy
Write data to DATA register//將數據寫入i/o設備的數據寄存器
Write command to COMMAND register//將命令寫入設備的命令寄存器
(Doing so starts the device and executes the command)//設備開始執行該i/o請求
While (STATUS == BUSY)//等待設備的空閑
; // wait until device is done with your request

該協議有四個步驟。

首先,OS等待,直到設備為準備接收命令的狀態(通過循環不斷重複讀取狀態寄存器來獲取設備狀態);我們稱之為輪詢設備(基本上,只是詢問它發生了什麼)。

然後,OS將一些數據發送到數據寄存器。當CPU參與數據移動時(如本示例協議中所述),我們將其稱為編程I / O(programmed I/O,PIO)。

第三,OS將命令寫入命令??寄存器;這樣做會隱式地讓設備知道數據都存在並且它應該開始處理。

最後,操作系統等待設備完成,再次輪詢它,等待最終執行的結果(然後它可能會得到一個代碼來指示成功或失敗)。

這個基本協議具有簡單和有效的優點。但是,也存在一些低效率和不方便的地方。協議第一個問題是輪詢效率低下;特別是,它等待(可能很慢的)設備完成其活動而浪費了大量的CPU時間,而不是切換到另一個就緒進程,從而更好地利用CPU。那麼怎麼解決這個問題呢?

通過中斷機制可以解決輪詢慢的問題。 操作系統可以發出請求,將調用進程置於休眠狀態,並將上下文切換到另一個任務,而不是重複輪詢設備。當設備最終完成操作時,它將引發硬體中斷,導致CPU去執行預定的中斷服務程序(ISR)或更簡單的中斷處理程序。處理程序只是一個操作系統代碼,它將接受中斷(例如,通過從設備讀取數據和可能的錯誤代碼)並喚醒等待I / O的進程。因此,中斷允許cpu運行和I / O操作的重疊,這是提高利用率的關鍵。 此時間線顯示如下:

在圖中,進程1在CPU上運行一段時間(由CPU行上的重複1表示),然後向磁碟發出I / O請求以讀取一些數據。 在沒有中斷的情況下,系統只需重複輪詢設備的狀態,直到I / O完成(由p表示)。磁碟完成請求後進程1可以再次運行。

如果我們使用中斷,操作系統可以在等待磁碟時執行其他操作:

在上圖中,操作系統在CPU上運行進程2,而磁碟服務進程1的請求。 磁碟請求完成後,會發生中斷,操作系統喚醒進程1並再次運行它。因此,CPU和磁碟在中間段時間內都得到了合適的利用。

需要注意的是,使用中斷並不總是最佳的解決方案。 例如,假設一個設備可以非常快速地執行其任務:輪詢第一次的時候請求就完成了。在這種情況下使用中斷實際上會降低系統速度:因為切換到另一個進程,處理中斷,然後再切換回原來的進程的代價是很昂貴的。因此,如果設備速度很快,最好進行輪詢; 如果它很慢,允許中斷是最好的。如果設備的速度未知,或者有時速度快,有時速度慢,最好使用輪詢和中斷的混合,首先先輪詢一段時間,如果設備尚未完成,則使用中斷。

不使用中斷的另一個場景是在網路伺服器中。當大量傳入數據包每個都產生一個中斷時,操作系統可能會活鎖,也就是說,os只處理中斷,從不允許用戶級進程運行並實際為請求提供服務。例如,想像一個經歷負載突發的Web伺服器,可能是因為它成為了Hacker News中排名第一的條目。在這種情況下,最好偶爾使用輪詢來更好地控制系統中發生的事情,並允許Web伺服器在返回檢查是否有更多的數據包到達之前為某些請求提供服務。另一種基於中斷的優化是合併。在這樣的設置中,需要引發中斷的設備在將中斷傳遞給CPU之前首先等待一會。在等待期間,可能會再收到中斷,最終可以將這些中斷進行合併再統一傳遞,這樣可以降低中斷處理的開銷。當然,等待太久會增加請求的延遲,這個時間可以試具體情況而定。

我們的規範協議還有另外一個方面需要我們注意。特別是,當使用PIO將大量數據傳輸到設備時,CPU再次承擔了相當繁瑣的任務負擔,因此浪費了大量時鐘週期,此時間表說明瞭問題:

在時間線中,進程1正在運行,然後希望將一些數據寫入磁碟。接著它啟動I / O,cpu必須將數據從內存複製到設備中,一次一個字(在圖中標記為c)。複製完成後,磁碟開始工作,這個時候CPU纔可用於其他操作。所以怎麼樣減少這種複製數據的損耗呢?

解決方案就是直接內存訪問(Direct Memory Access,DMA)。DMA引擎本質上是系統中一個特定的設備,它可以在沒有太多CPU幹預的情況下協調設備和主存之間的傳輸。DMA的工作原理如下。 例如,要將數據傳輸到設備,操作系統對DMA引擎進行編程來告知數據存儲在內存中的位置,要複製的數據量以及將數據發送到哪個設備。這樣就相當於操作系統完成了傳輸,可以繼續進行其他工作。然後DMA開始工作,DMA完成後,DMA控制器會產生中斷,因此操作系統知道傳輸完成。修訂後的時間表:

可以看到數據的複製現在由DMA控制器處理。 由於CPU在此期間是空閑的,因此操作系統可以執行其他操作,此處選擇運行進程2。因此,在進程1再次運行之前,進程2可以使用更多的CPU。

現在我們已經瞭解了執行I / O所涉及的效率問題,我們需要處理一些其他問題才能將設備整合到現代系統中。到目前為止你可能已經注意到的一個問題:我們還沒有真正說過操作系統如何與設備進行實際通信!因此,現在面臨的問題是:

怎麼和硬體設備進行通信? 應該是明確的指令嗎? 或者還有其他方法嗎?

在計算機發展的歷程中,有兩種主要的設備通信方法。第一個最古老的方法(多年來由IBM大型機使用)是有明確的I / O指令。這些指令指定OS將數據發送到特定設備寄存器的方式。例如,在x86上,in和out指令可用於與設備通信。要將數據發送到設備,調用者將數據放入指定寄存器,以及通過特定的埠來指定設備。然後執行指令就可以了。這些指示通常是特權指令。所以操作系統控制設備,是唯一允許與它們直接通信的實體。與設備交互的第二種方法稱為內存映射I / O.通過這種方法,對硬體的寄存器的訪問就像訪問內存一樣。為了訪問特定寄存器,OS對指定區域的內存讀取或寫入,然後硬體將該數據讀取/寫入設備中。這2種方式沒有孰優孰劣,內存映射方法不需要新指令來支持它(這是優勢),但這兩種方法至今仍在使用。

我們將討論的最後一個問題是:如何將每個都具有非常特定介面的設備安裝到操作系統中,我們希望這種安裝方式能儘可能保持通用。例如,我們想構建一個在SCSI磁碟,IDE磁碟,usb等設備之上工作的文件系統,我們希望文件系統能夠相對忽略掉一些具體的設備細節。這個問題是通過古老的抽象技術解決的。在最低級別,操作系統中的特定軟體必須詳細瞭解設備的工作原理。我們將這個軟體稱為設備驅動程序,設備驅動程序將與設備交互的任何細節都封裝在其中。讓我們通過Linux文件系統來看看抽象如何來幫助OS設計和實現。圖36.4是Linux軟體組織的粗略描述。

從圖中可以看出,文件系統(當然還有上面的應用程序)完全忽略了它所使用的磁碟的細節;它只是向通用塊層發出塊讀取和寫入請求,通用通用層將它們路由到適當的設備驅動程序,驅動程序來實際處理請求。雖然簡化了,但該圖顯示了大多數操作系統中隱藏的細節。該圖還顯示了設備的原始介面,可以使特殊的應用程序(例如文件系統檢查程序,或磁碟碎片整理工具)能夠直接讀取和寫入塊,而無需使用文件抽象。大多數系統都提供此類介面來支持這些低層級存儲管理應用程序。

需要注意的是上面看到的封裝也有其缺點。例如,如果某個設備具有許多特殊功能,但又必須向內核的其餘部分提供通用介面,那麼這些特殊功能就不能使用了。例如,SCSI設備具有非常豐富的錯誤報告;因為其他塊設備(例如,ATA / IDE)只有簡單的錯誤報告,所以更高級別的軟體接收的就是通用EIO(通用IO錯誤)錯誤代碼;因此,SCSI可能提供的任何額外細節都會丟失。有趣的是,因為你插入系統的任何設備都需要設備驅動程序,隨著時間的推移,它們代表了很大比例的內核代碼。對Linux內核的研究表明,超過70%的OS代碼存在於設備驅動程序中;對於基於Windows的系統,它可能更高。因此,當人們告訴你操作系統有數百萬行代碼時,他們真正說的是操作系統有數百萬行設備驅動程序代碼。也許更令人沮喪的是,由於驅動程序通常由「業餘愛好者」(而不是全職內核開發人員)編寫,它們往往會有更多的錯誤,因此是內核崩潰的主要原因。

為了深入學習,讓我們快速瀏覽一下實際設備:IDE磁碟驅動器。

IDE磁碟提供了簡單的系統介面,包括四種類型的寄存器:控制,命令塊,狀態和錯誤。使用(在x86上)in和out I / O指令,可以訪問這些寄存器。假設已經初始化,與設備交互的基本協議如下。

1.等待驅動器做好準備。讀取狀態寄存器(0x1F7)直到驅動器狀態不是busy而是ready。

2.將參數寫入命令寄存器。扇區數,要訪問的扇區的邏輯塊地址(LBA)和驅動器號(master = 0x00或slave = 0x10,因為IDE只允許兩個驅動器),命令寄存器對應的編號是(0x1F2-0x1F6)。

3.啟動I / O。對命令寄存器0x1F7寫入READ—WRITE 命令。

4.數據傳輸(用於寫入):等待驅動器狀態為READY和DRQ(數據驅動請求);將數據寫入數據埠。

5.處理中斷。在最簡單的情況下,處理每個數據塊傳輸的中斷;更複雜的方法允許批處理,當整個傳輸完成時,才發出一次中斷。

6.錯誤處理。每次操作後,讀取狀態寄存器。如果ERROR位有置位,讀取錯誤寄存器以獲取詳細信息。

上面說到的協議的內容大多數都可以在xv6 IDE驅動程序中找到(圖36.6),該驅動程序(初始化後)通過四個主要函數工作。第一個是ide_rw(),這個方法首先將I/O請求組成一個隊列(如果有其他的等待處理的請求),或者直接通過ide_start_request()方法處理請求,在這2種情況下,調用的進程都會進入睡眠,等待請求的完成。對於ide_start_request()方法,它是用來處理硬碟收到的請求的。使用in和out指令分別去讀取和寫入設備寄存器。在這個方法中還會調用ide_wait_ready()方法來判斷設備的狀態是否是ready。最後,當中斷產生的時候調用ide_intr() ,這個方法從設備中讀取數據(如果產生中斷的請求是讀任務),喚醒等待I/O的進程,並且如果還有等待執行的請求,調用ide_start_request()方法執行下一個I/O請求。

推薦閱讀:

相關文章