咱們先來看一個典型計算機系統的架構。其中,CPU經過某種內存總線(memory bus)或互連電纜鏈接到系統內存。圖像或者其餘高性能I/O設備經過常規的I/O總線(I/O bus)鏈接到系統,在許多現代系統中會是PCI或它的衍生形式。更下面是外圍總線(peripheral bus),好比SCSI、SATA或者USB。它們將最慢的設備鏈接到系統,包括磁盤、鼠標及其餘相似設備。算法
爲何要用這樣的分層架構?緣由在於物理佈局及造價成本。越快的總線越短,所以高性能的內存總線沒有足夠的空間鏈接太多設備。另外,在工程上高性能總線的造價很是高。因此,系統的設計採用了這種分層的方式,這樣可讓要求高性能的設備離CPU更近一些,低性能的設備離CPU遠一些。將磁盤和其餘低速設備連到外圍總線的好處不少,最重要的就是能夠在外圍總線上鏈接大量的設備。緩存
如今來看一個標準設備(不是真實存在的),經過它來幫助咱們更好地理解設備交互的機制。從圖中能夠看到一個包含兩個重要組件的設備。第一部分是向系統其餘部分展示的硬件接口(interface),讓系統軟件來控制它的操做。所以,全部設備都有本身的特定接口以及典型的交互協議。網絡
第二部分是它的內部結構(internal structure)。這部分包含設備展現給系統的抽象接口相關的特定實現,很是簡單的設備一般用一個或幾個芯片來實現它們的功能。更復雜的設備會包含簡單的CPU、一些通用內存、設備相關的特定芯片,來完成它們的工做。架構
在上圖中,一個簡化的設備接口包含3個寄存器:一個狀態(status)寄存器,能夠讀取並查看設備的當前狀態;一個命令(command)寄存器,用於通知設備執行某個具體任務;一個數據(data)寄存器,將數據傳給設備或從設備接收數據。經過讀寫這些寄存器,操做系統能夠控制設備的行爲。app
操做系統與該設備的典型交互協議以下:佈局
While (STATUS == BUSY) ; // wait until device is not busy Write data to DATA register Write command to COMMAND register (Doing so starts the device and executes the command) While (STATUS == BUSY) ; // wait until device is done with your request
該協議包含4步。性能
這個協議的好處是足夠簡單而且有效,可是不免會有一些低效和不方便。咱們注意到輪詢會致使在等待設備執行完成時浪費大量CPU時間,若是此時操做系統能夠切換執行下一個就緒進程,就能夠大大提升CPU的利用率。優化
多年前,工程師們發明了咱們目前已經很常見的中斷(interrupt)來減小CPU開銷。有了中斷後,CPU 再也不須要不斷輪詢設備,而是向設備發出一個請求,而後就可讓對應進程睡眠,切換執行其餘任務。當設備完成了自身操做,會拋出一個硬件中斷,引起CPU跳轉執行操做系統預先定義好的中斷服務例程(Interrupt Service Routine,ISR),或更爲簡單的中斷處理程序(interrupthandler)。中斷處理程序是一小段操做系統代碼,它會結束以前的請求(好比從設備讀取到了數據或者錯誤碼)而且喚醒等待I/O的進程繼續執行。編碼
不過,使用中斷並不是老是最佳方案。假若有一個很是高性能的設備,它處理請求很快:一般在CPU第一次輪詢時就能夠返回結果。此時若是使用中斷,反而會使系統變慢:切換到其餘進程,處理中斷,再切換回以前的進程會有必定的代價。所以,若是設備很是快,那麼最好的辦法反而是輪詢。若是設備比較慢,那麼採用中斷更好。若是設備的速度未知,或者時快時慢,能夠考慮使用混合(hybrid)策略,先嚐試輪詢一小段時間,若是設備沒有完成操做,此時再使用中斷。spa
另外一個最好不要使用中斷的場景是網絡。網絡端收到大量數據包,若是每個包都發生一次中斷,那麼有可能致使操做系統發生活鎖(livelock),即不斷處理中斷而沒法處理用戶層的請求。
還有一個基於中斷的優化就是合併(coalescing)。設備在拋出中斷以前每每會等待一小段時間,在此期間,其餘請求可能很快完成,所以屢次中斷能夠合併爲一次中斷拋出,從而下降處理中斷的代價。固然,等待太長會增長請求的延遲,這是系統中常見的折中。
標準協議還有一點須要咱們注意。若是使用這種方式,CPU的時間會浪費在向設備傳輸數據或從設備傳出數據的過程當中。如何才能分離這項工做,從而提升CPU的利用率?
解決方案就是使用DMA(Direct Memory Access)。DMA引擎是系統中的一個特殊設備,它能夠協調完成內存和設備間的數據傳遞,不須要CPU介入。
DMA工做過程以下:爲了可以將數據傳送給設備,操做系統會經過程序告訴DMA引擎數據在內存的位置,要拷貝的大小以及要拷貝到哪一個設備。在此以後,操做系統就能夠處理其餘請求了。當DMA的任務完成後,DMA控制器會拋出一箇中斷來告訴操做系統已經完成數據傳輸。
在瞭解瞭如何提高I/O的效率後,咱們來討論一下操做系統究竟如何與設備通訊。
隨着技術的不斷髮展,目前主要有兩種方式來實現與設備的交互。第一種辦法相對古老一些,就是用明確的I/O指令。這些指令規定了操做系統將數據發送到特定設備寄存器的方法,從而容許構造上文提到的協議。
例如在x86上,in和out指令能夠用來與設備進行交互。當須要發送數據給設備時,調用者指定一個存入數據的特定寄存器及一個表明設備的特定端口。執行這個指令就能夠實現指望的行爲。
第二種方法是內存映射I/O(memory-mapped I/O)。經過這種方式,硬件將設備寄存器做爲內存地址提供。當須要訪問設備寄存器時,操做系統裝載或者存入到該內存地址;而後硬件會將裝載/存入轉移到設備上,而不是物理內存。
兩種方法都沒有相對明顯的優點。內存映射I/O的好處是不須要引入新指令來實現設備交互,但兩種方法今天都在使用。
還有最後一個問題:每一個設備都有很是具體的接口,那麼如何將它們歸入操做系統,而且讓操做系統儘量通用呢?
這個問題能夠經過古老的抽象技術來解決。在最底層,操做系統的一部分軟件清楚地知道設備如何工做,咱們將這部分軟件稱爲設備驅動程序(device driver),全部設備交互的細節都封裝在其中。
咱們來看看Linux文件系統棧,理解抽象技術如何應用於操做系統的設計和實現。下圖粗略地展現了Linux軟件的組織方式。能夠看出,文件系統(固然也包括在其之上的應用程序)徹底不清楚它使用的是什麼類型的磁盤。它只須要簡單地向通用塊設備層發送讀寫請求便可,塊設備層會將這些請求路由給對應的設備驅動,而後設備驅動來完成真正的底層操做。
介紹了通用的I/O設備概念後,咱們將更詳細地介紹一種設備:磁盤驅動器(hard disk drive)。磁盤驅動器一直是計算機系統中持久化數據存儲的主要形式,文件系統技術的發展大部分都是基於它們的行爲。所以,在構建管理它的文件系統軟件以前,有必要先了解磁盤操做的細節。
全部現代驅動器的基本接口都很簡單。驅動器由大量扇區(512字節塊)組成,每一個扇區均可以讀取或寫入。在包含n個扇區的磁盤上,扇區從0到n−1編號。所以,咱們能夠將磁盤視爲一組扇區,0到n−1是驅動器的地址空間(address space)。
多扇區操做是可能的,實際上許多文件系統一次讀取或寫入4KB(或更多)。但在更新磁盤時,驅動器製造商惟一保證的是單個扇區的寫入是原子的。所以,若是發生不合時宜的掉電,則只能完成較大寫入的一部分 (有時稱爲不完整寫入)。
一般能夠假設訪問驅動器地址空間內的連續塊(即順序讀取或寫入)是最快的訪問模式,而且一般比任何更隨機的訪問模式快得多。
一個磁盤可能有一個或多個盤片(platter),每一個盤片有兩面,稱爲表面。這些盤片一般由一些硬質材料(如鋁)製成,而後塗上薄薄的磁性層,即便驅動器斷電,驅動器也能持久存儲數據位。
全部盤片都圍繞主軸(spindle)鏈接在一塊兒,主軸以一個恆定的速度旋轉盤片。旋轉速率一般以每分鐘轉數(Rotations Per Minute,RPM)來測量,典型的現代數值在7200~15000 RPM範圍內。
數據在扇區的同心圓中的每一個表面上被編碼,咱們稱這樣的同心圓爲一個磁道(track)。一個表面包含數以千計的磁道,緊密地排在一塊兒。
磁盤的讀寫過程由磁頭(disk head)完成,驅動器的每一個表面有一個這樣的磁頭。磁頭鏈接到單個磁盤臂(disk arm)上,磁盤臂在表面上移動,將磁頭定位在指望的磁道上。
讓咱們每次都構建一個簡單的模型,來了解磁盤是如何工做的。首先假設咱們有一個單一磁道的簡單磁盤。該磁道只有12個扇區,每一個扇區的大小爲512字節,用0到11的數字表示。
假設咱們如今收到讀取塊0的請求,磁盤應如何處理該請求?
具體來講,它必須等待指望的扇區旋轉到磁頭下。這種等待在現代驅動器中常常發生,而且是I/O服務時間的重要組成部分,它有一個特殊的名稱:旋轉延遲(rotational delay,有時稱爲rotation delay)。在這個例子中,若是完整的旋轉延遲是R,那麼磁盤必然產生大約爲R/2的旋轉延遲,以等待0來到讀/寫磁頭下面。
磁盤只有一條磁道,這是不太現實的,現代磁盤有數以百萬計的磁道。所以,咱們來看看更現實一點的磁盤表面,這個表面有3條磁道。磁頭當前位於最內圈的磁道上。下一個磁道包含下一組扇區,最外面的磁道包含最前面的扇區。
假設如今須要讀取扇區11。爲了服務這個讀取請求,驅動器必須首先將磁盤臂移動到正確的磁道,這個過程也即所謂的尋道(seek)。尋道和旋轉都是最昂貴的磁盤操做之一。
尋道有許多階段:首先是磁盤臂移動時的加速階段,隨着磁盤臂全速移動而慣性滑動,而後隨着磁盤臂減速而減速,最後在正確的磁道上停下來。
在尋道過程當中,盤片也會跟着一塊兒旋轉,在這個例子中,大約旋轉了3個扇區。所以,尋道完成時,磁頭下方是扇區9。接着咱們只需再等待短暫的轉動延遲,當扇區11通過磁頭時,數據纔開始真正讀寫,稱爲傳輸(transfer)。所以,咱們獲得了完整的I/O時間圖:首先尋道,而後等待轉動延遲,最後傳輸。
許多驅動器採用某種形式的磁道偏斜(track skew),以確保即便在跨越磁道邊界時,也能夠很方便地順序讀取。從一個磁道切換到另外一個磁道時,若是沒有這種偏斜,所需的下一個塊已經旋轉到磁頭後的位置,所以驅動器將不得不等待整個旋轉延遲,才能訪問下一個塊。
外圈磁道一般比內圈磁道具備更多扇區,這是幾何結構的結果。這一般被稱爲多區域(multi-zoned)磁盤驅動器,其中磁盤被組織成多個區域,區域是表面上連續的一組磁道。每一個區域每一個磁道具備相同的扇區數量,而且外圈區域具備比內圈區域更多的扇區。
任何現代磁盤驅動器都有一個重要組成部分,即它的緩存(cache),因爲歷史緣由有時稱爲磁道緩衝區(track buffer)。該緩存只是少許的內存(一般大約8MB或16MB),驅動器可使用這些內存來保存從磁盤讀取或寫入磁盤的數據。例如,當從磁盤讀取扇區時,驅動器可能決定讀取該磁道上的全部扇區並將其緩存在其存儲器中。這樣作可讓驅動器快速響應全部後續對同一磁道的請求。
在寫入時,驅動器面臨一個選擇:它應該在將數據放入其內存以後仍是實際寫入磁盤以後,回報寫入完成?前者被稱爲後寫(write back)緩存,後者則稱爲直寫(write through)。後寫緩存有時會使驅動器看起來「更快」,但可能有風險。若是文件系統或應用程序要求將數據按特定順序寫入磁盤以保證正確性,後寫緩存可能會致使問題。
關於磁盤驅動器的有關I/O的時間和性能問題,咱們只須要記住如下幾點:
因爲I/O的高成本,操做系統在決定發送給磁盤的I/O順序方面從來發揮重要做用。給定一組I/O請求,磁盤調度程序檢查請求並決定下一個要調度的請求。
與任務調度不一樣,對於磁盤調度,咱們能夠很好地猜想磁盤請求須要多長時間。經過估計請求的查找和可能的旋轉延遲,磁盤調度程序能夠知道每一個請求將花費多長時間,所以選擇先服務花費最少時間的請求。所以,磁盤調度程序將嘗試在其操做中遵循SJF(最短任務優先)的原則。
一種早期的磁盤調度方法被稱爲最短尋道時間優先(Shortest-Seek-Time-First,SSTF)。SSTF按磁道對I/O請求隊列排序,選擇在最近磁道上的請求先完成。例如,假設磁頭當前位置在內圈磁道上,而且咱們請求扇區21和2,那麼咱們會首先發出對21的請求,等待它完成,而後發出對2的請求。
在這個例子中,SSTF運做良好,首先尋找中間磁道,而後尋找外圈磁道。但SSTF不是萬能的,緣由以下。第一個問題,操做系統沒法知道驅動器的幾何結構,而是隻會看到一系列的塊。不過這個問題很容易解決。操做系統能夠簡單地實現最近塊優先(Nearest-Block-First,NBF),而後用最近的塊地址來調度請求。
第二個問題更爲根本:飢餓(starvation)。在咱們上面的例子中,若是有對磁頭當前所在位置的內圈磁道有穩定的請求,純粹的SSTF方法則將徹底忽略對其餘磁道的請求。
電梯算法也是一種比較古老的算法。該算法最初稱爲SCAN,簡單地以跨越磁道的順序來服務磁盤請求。咱們將一次跨越磁盤稱爲「掃一遍」,若是請求的塊所屬的磁道在此次「掃一遍」中已經服務過了,它就不會當即處理,而是排隊等待下次「掃一遍」。
SCAN有許多變種,C-SCAN是一種常見的變體,即循環SCAN(Circular SCAN)的縮寫。該算法不是在一個方向掃過磁盤,而是從外圈掃到內圈,而後從內圈掃到外圈,如此循環往復。
然而,SCAN及其變種並非最好的調度技術。特別是,SCAN還有SSTF實際上並無嚴格遵照SJF的原則,由於它們忽視了旋轉時間。
爲了更好地理解這種算法,咱們來看一個例子。
在這個例子中,磁頭當前定位在內圈磁道上的扇區30上方。此時有兩個分別針對扇區8和16的I/O請求所以,調度程序如何決定接下來應該服務哪一個請求?
答案是「視狀況而定」。這裏須要考慮的狀況是旋轉與尋道相比的相對時間。若是在咱們的例子中,尋道時間遠遠高於旋轉延遲,那麼SSTF就行了。可是,若是尋道比旋轉快得多,那麼在咱們的例子中,尋道遠一點的、在外圈磁道的服務請求8,比尋道近一點的、在中間磁道的服務請求16更好。
在現代驅動器中,查找和旋轉大體至關,所以SPTF是有用的,它提升了性能。然而,它在操做系統中實現起來更加困難,操做系統一般不太清楚磁道邊界在哪,也不知道磁頭當前的位置(旋轉到了哪裏)。所以,SPTF一般在驅動器內部執行。
在較早的系統中,操做系統完成了全部的磁盤調度。然而在現代系統中,磁盤能夠接受多個分離的請求,並且它們自己具備複雜的內部調度程序。所以,操做系統調度程序一般會選擇它認爲最好的幾個請求,並將它們所有發送到磁盤。磁盤而後利用其磁頭位置和詳細的磁道佈局信息等內部知識,以最佳可能(SPTF)順序服務於這些請求。
磁盤調度程序執行的另外一個重要相關任務是I/O合併(I/O merging)。例如,設想一系列請求讀取塊33,而後是8,而後是34。在這種狀況下,調度程序應該將塊33和34的請求合併(merge)爲單個兩塊請求。調度程序執行的全部請求都基於合併後的請求。合併在操做系統級別尤爲重要,由於它減小了發送到磁盤的請求數量,從而下降了開銷。
現代調度程序關注的最後一個問題是:在向磁盤發出I/O以前,系統應該等待多久?有人認爲,即便只有一個磁盤I/O,也應當即向驅動器發出請求,這種方法被稱爲工做保全(work-conserving)。然而,研究代表,有時最好等待一段時間,即所謂的非工做保全(non-work-conserving)方法。經過等待,新的或「更好」的請求可能會到達磁盤,從而提升總體效率。固然,決定什麼時候等待以及等待多久,可能會很棘手,須要大量實踐和觀察。