1.JAVA NIO簡介

Java NIO

第一章 簡介

1.I/O與CPU時間的比較

 

2.CPU已不再是束縛

如今在運行時優化方面,JVM 已然前進了一大步。現在 JVM 運行字節碼的速率已經接近本地 編譯代碼,藉助動態運行時優化,其表現甚至還有所超越。這就意味着,多數 Java 應用程序已不 再受 CPU 的束縛(把大量時間用在執行代碼上), 而更多時候是受 I/O 的束縛(等待數據傳 輸)。

然而,在大多數情況下,Java 應用程序並非真的受着 I/O 的束縛。操作系統並非不能快速傳送 數據,讓 Java 有事可做;相反,是 JVM 自身在 I/O 方面效率欠佳。操作系統與 Java 基於流的 I/O 模型有些不匹配。 操作系統要移動的是大塊數據(緩衝區), 這往往是在硬件直接存儲器存取 (DMA)的協助下完成的。而 JVM 的 I/O 類喜歡操作小塊數據——單個字節、幾行文本。結果, 操作系統送來整緩衝區的數據,java.io 的流數據類再花大量時間把它們拆成小塊,往往拷貝一 個小塊就要往返於幾層對象。操作系統喜歡整卡車地運來數據,java.io 類則喜歡一鏟子一鏟子 地加工數據。有了 NIO,就可以輕鬆地把一卡車數據備份到您能直接使用的地方(ByteBuffer 對 象)。

這並不是說使用傳統的 I/O 模型無法移動大量數據——當然可以(現在依然可以)。具體地 說,RandomAccessFile 類在這方面的效率就不低,只要堅持使用基於數組的 read( )和 write( )方法。 這些方法與底層操作系統調用相當接近,儘管必須保留至少一份緩衝區拷貝。

如表 1-1 所示,如果您的代碼大部分時間都處於 I/O 等待狀態,那麼,該考慮一下提升 I/O 效 率的問題了,否則,您精心打造的代碼多數時間都得閒着。

3.Java NIO

java.nio 軟件包提供了新的抽象。具體地說,就是 Channel 和 Selector 類。它們提供了使用 I/O 服務的通用 API,JDK 1.4 以前的版本是無法使用這些服務的。天下還是 沒有免費的午餐:您無法使用每一種操作系統的每一種特性,但是這些新類還是提供了強大的新框 架,涵蓋了當今商業操作系統普遍提供的高效 I/O 特性。不僅如此,java.nio.channels.spi 還提供了新的服務提供接口(SPI),允許接入新型通道和選擇器,同時又不違反規範的一致性。

4.IO概念

JDK 1.4 的 NIO 軟件包引入了一套新的抽象用於 I/O 處理。與以往不同的是,新的抽象把重點放在瞭如何縮短抽象與現實之間的距離上 面。NIO 抽象與現實中存在的實體有着非常真實直接的交互關係。要想最大限度地滿足 Java 應用 程序的密集 I/O 需求,理解這些新的抽象,以及與其發生交互作用的 I/O 服務(其重要性並不亞於 抽象),正是關鍵所在。

  • 理解以下概念是非常重要的:

    • 緩衝區操作

    • 內核空間與用戶空間

    • 虛擬內存

    • 分頁技術

    • 面向文件的 I/O 和流 I/O

    • 多工 I/O(就緒性選擇)

4.1緩衝區操作

圖 1-1 簡單描述了數據從外部磁盤向運行中的進程的內存區域移動的過程。進程使用 read( )系 統調用,要求其緩衝區被填滿。內核隨即向磁盤控制硬件發出命令,要求其從磁盤讀取數據。磁盤 控制器把數據直接寫入內核內存緩衝區,這一步通過 DMA 完成,無需主 CPU 協助。一旦磁盤控 制器把緩衝區裝滿,內核即把數據從內核空間的臨時緩衝區拷貝到進程執行 read( )調用時指定的緩 衝區。

注意圖中用戶空間和內核空間的概念。用戶空間是常規進程所在區域。JVM 就是常規進程, 駐守於用戶空間。用戶空間是非特權區域:比如,在該區域執行的代碼就不能直接訪問硬件設備。 內核空間是操作系統所在區域。內核代碼有特別的權力:它能與設備控制器通訊,控制着用戶區域 進程的運行狀態,等等。最重要的是,所有 I/O 都直接(如這裏所述)或間接(見 1.4.2 小節)通 過內核空間。

當進程請求 I/O 操作的時候,它執行一個系統調用(有時稱爲陷阱)將控制權移交給內核。 C/C++程序員所熟知的底層函數 open( )、read( )、write( )和 close( )要做的無非就是建立和執行適當的系統調用。當內核以這種方式被調用,它隨即採取任何必要步驟,找到進程所需數據,並把數據 傳送到用戶空間內的指定緩衝區。內核試圖對數據進行高速緩存或預讀取,因此進程所需數據可能 已經在內核空間裏了。如果是這樣,該數據只需簡單地拷貝出來即可。如果數據不在內核空間,則 進程被掛起,內核着手把數據讀進內存。

看了圖 1-1,您可能會覺得,把數據從內核空間拷貝到用戶空間似乎有些多餘。爲什麼不直接 讓磁盤控制器把數據送到用戶空間的緩衝區呢?這樣做有幾個問題。首先,硬件通常不能直接訪問 用戶空間 1 。其次,像磁盤這樣基於塊存儲的硬件設備操作的是固定大小的數據塊,而用戶進程請 求的可能是任意大小的或非對齊的數據塊。在數據往來於用戶空間與存儲設備的過程中,內核負責 數據的分解、再組合工作,因此充當着中間人的角色。

1)發散/匯聚

許多操作系統能把組裝/分解過程進行得更加高效。根據發散/匯聚的概念,進程只需一個系 統調用,就能把一連串緩衝區地址傳遞給操作系統。然後,內核就可以順序填充或排幹多個緩衝 區,讀的時候就把數據發散到多個用戶空間緩衝區,寫的時候再從多個緩衝區把數據匯聚起來(圖 1-2)。

這樣用戶進程就不必多次執行系統調用(那樣做可能代價不菲),內核也可以優化數據的處理 過程,因爲它已掌握待傳輸數據的全部信息。如果系統配有多個 CPU,甚至可以同時填充或排幹 多個緩衝區。

4.2虛擬內存

所有現代操作系統都使用虛擬內存。虛擬內存意爲使用虛假(或虛擬)地址取代物理(硬件 RAM)內存地址。這樣做好處頗多,總結起來可分爲兩大類:

  1. 一個以上的虛擬地址可指向同一個物理內存地址。

  2. 虛擬內存空間可大於實際可用的硬件內存。

 

前一節提到,設備控制器不能通過 DMA 直接存儲到用戶空間,但通過利用上面提到的第一 項,則可以達到相同效果。把內核空間地址與用戶空間的虛擬地址映射到同一個物理地址,這樣, DMA 硬件(只能訪問物理內存地址)就可以填充對內核與用戶空間進程同時可見的緩衝區(見圖 1-3)。

這樣真是太好了,省去了內核與用戶空間的往來拷貝,但前提條件是,內核與用戶緩衝區必須 使用相同的頁對齊,緩衝區的大小還必須是磁盤控制器塊大小(通常爲 512 字節磁盤扇區)的倍 數。操作系統把內存地址空間劃分爲頁,即固定大小的字節組。內存頁的大小總是磁盤塊大小的倍 數,通常爲 2 次冪(這樣可簡化尋址操作)。典型的內存頁爲 1,024、2,048 和 4,096 字節。虛擬和 物理內存頁的大小總是相同的。圖 1-4 顯示了來自多個虛擬地址的虛擬內存頁是如何映射到物理內存的。

4.3內存頁面調度

爲了支持虛擬內存的第二個特性(尋址空間大於物理內存),就必須進行虛擬內存分頁(經常 稱爲交換,雖然真正的交換是在進程層面完成,而非頁層面)。依照該方案,虛擬內存空間的頁面 能夠繼續存在於外部磁盤存儲,這樣就爲物理內存中的其他虛擬頁面騰出了空間。從本質上說,物 理內存充當了分頁區的高速緩存;而所謂分頁區,即從物理內存置換出來,轉而存儲於磁盤上的內 存頁面。

圖 1-5 顯示了分屬於四個進程的虛擬頁面,其中每個進程都有屬於自己的虛擬內存空間。進程 A 有五個頁面,其中兩個裝入內存,其餘存儲於磁盤。

把內存頁大小設定爲磁盤塊大小的倍數,這樣內核就可直接向磁盤控制硬件發佈命令,把內存 頁寫入磁盤,在需要時再重新裝入。結果是,所有磁盤 I/O 都在頁層面完成。對於採用分頁技術的 現代操作系統而言,這也是數據在磁盤與物理內存之間往來的唯一方式。

現代 CPU 包含一個稱爲內存管理單元(MMU)的子系統,邏輯上位於 CPU 與物理內存之 間。該設備包含虛擬地址向物理內存地址轉換時所需映射信息。當 CPU 引用某內存地址時,MMU 負責確定該地址所在頁(往往通過對地址值進行移位或屏蔽位操作實現),並將虛擬頁號轉換爲物 理頁號(這一步由硬件完成,速度極快)。如果當前不存在與該虛擬頁形成有效映射的物理內存 頁,MMU 會向 CPU 提交一個頁錯誤。

頁錯誤隨即產生一個陷阱(類似於系統調用),把控制權移交給內核,附帶導致錯誤的虛擬地 址信息,然後內核採取步驟驗證頁的有效性。內核會安排頁面調入操作,把缺失的頁內容讀回物理 內存。這往往導致別的頁被移出物理內存,好給新來的頁讓地方。在這種情況下,如果待移出的頁已經被碰過了(自創建或上次頁面調入以來,內容已發生改變),還必須首先執行頁面調出,把頁 內容拷貝到磁盤上的分頁區。

如果所要求的地址不是有效的虛擬內存地址(不屬於正在執行的進程的任何一個內存段),則 該頁不能通過驗證,段錯誤隨即產生。於是,控制權轉交給內核的另一部分,通常導致的結果就是 進程被強令關閉。

一旦出錯的頁通過了驗證,MMU 隨即更新,建立新的虛擬到物理的映射(如有必要,中斷被 移出頁的映射),用戶進程得以繼續。造成頁錯誤的用戶進程對此不會有絲毫察覺,一切都在不知 不覺中進行。

4.文件IO

文件 I/O 屬文件系統範疇,文件系統與磁盤迥然不同。磁盤把數據存在扇區上,通常一個扇區 512 字節。磁盤屬硬件設備,對何謂文件一無所知,它只是提供了一系列數據存取窗口。在這點 上,磁盤扇區與內存頁頗有相似之處:都是統一大小,都可作爲大的數組被訪問。

文件系統是更高層次的抽象,是安排、解釋磁盤(或其他隨機存取塊設備)數據的一種獨特方 式。您所寫代碼幾乎無一例外地要與文件系統打交道,而不是直接與磁盤打交道。是文件系統定義 了文件名、路徑、文件、文件屬性等抽象概念。

文件系統把一連串大小一致的數據塊組織到一起。有些塊存儲元信息,如空閒塊、目錄、索引 等的映射,有些包含文件數據。單個文件的元信息描述了哪些塊包含文件數據、數據在哪裏結束、 最後一次更新是什麼時候,等等。

當用戶進程請求讀取文件數據時,文件系統需要確定數據具體在磁盤什麼位置,然後着手把相 關磁盤扇區讀進內存。 老式的操作系統往往直接向磁盤驅動器發佈命令, 要求其讀取所需磁盤扇 區。而採用分頁技術的現代操作系統則利用請求頁面調度取得所需數據。

操作系統還有個頁的概念,其大小或者與基本內存頁一致,或者是其倍數。典型的操作系統頁 從 2,048 到 8,192 字節不等,且始終是基本內存頁大小的倍數。

 

  • 採用分頁技術的操作系統執行 I/O 的全過程可總結爲以下幾步:

    • 確定請求的數據分佈在文件系統的哪些頁(磁盤扇區組)。磁盤上的文件內容和元數 據可能跨越多個文件系統頁,而且這些頁可能也不連續。

    • 在內核空間分配足夠數量的內存頁,以容納得到確定的文件系統頁。

    • 在內存頁與磁盤上的文件系統頁之間建立映射。

    • 爲每一個內存頁產生頁錯誤。

    • 虛擬內存系統俘獲頁錯誤,安排頁面調入,從磁盤上讀取頁內容,使頁有效。

    • 一旦頁面調入操作完成,文件系統即對原始數據進行解析,取得所需文件內容或屬性 信息。

 

需要注意的是,這些文件系統數據也會同其他內存頁一樣得到高速緩存。對於隨後發生的 I/O 請求,文件數據的部分或全部可能仍舊位於物理內存當中,無需再從磁盤讀取即可重複使用。

大多數操作系統假設進程會繼續讀取文件剩餘部分,因而會預讀額外的文件系統頁。如果內存 爭用情況不嚴重,這些文件系統頁可能在相當長的時間內繼續有效。這樣的話,當稍後該文件又被 相同或不同的進程再次打開,可能根本無需訪問磁盤。這種情況您可能也碰到過:當重複執行類似 的操作,如在幾個文件中進行字符串檢索,第二遍運行得似乎快多了。

類似的步驟在寫文件數據時也會採用。這時,文件內容的改變(通過 write( ))將導致文件系 統頁變髒,隨後通過頁面調出,與磁盤上的文件內容保持同步。文件的創建方式是,先把文件映射 到空閒文件系統頁,在隨後的寫操作中,再將文件系統頁刷新到磁盤。

1)內存映射文件

傳統的文件 I/O 是通過用戶進程發佈 read( )和 write( )系統調用來傳輸數據的。爲了在內核空間 的文件系統頁與用戶空間的內存區之間移動數據,一次以上的拷貝操作幾乎總是免不了的。這是因 爲,在文件系統頁與用戶緩衝區之間往往沒有一一對應關係。但是,還有一種大多數操作系統都支 持的特殊類型的 I/O 操作,允許用戶進程最大限度地利用面向頁的系統 I/O 特性,並完全摒棄緩衝 區拷貝。這就是內存映射 I/O,如圖 1-6 所示。

  • 內存映射 I/O 使用文件系統建立從用戶空間直到可用文件系統頁的虛擬內存映射。這樣做有幾 個好處:

    • 用戶進程把文件數據當作內存,所以無需發佈 read( )或 write( )系統調用。

    • 當用戶進程碰觸到映射內存空間,頁錯誤會自動產生,從而將文件數據從磁盤讀進內存。如果用戶修改了映射內存空間,相關頁會自動標記爲髒,隨後刷新到磁盤,文件 得到更新。

    • 操作系統的虛擬內存子系統會對頁進行智能高速緩存,自動根據系統負載進行內存管 理。

    • 數據總是按頁對齊的,無需執行緩衝區拷貝。

    • 大型文件使用映射,無需耗費大量內存,即可進行數據拷貝。

 

虛擬內存和磁盤 I/O 是緊密關聯的,從很多方面看來,它們只是同一件事物的兩面。在處理大 量數據時,尤其要記得這一點。如果數據緩衝區是按頁對齊的,且大小是內建頁大小的倍數,那 麼,對大多數操作系統而言,其處理效率會大幅提升。

2)文件鎖定

文件鎖定機制允許一個進程阻止其他進程存取某文件,或限制其存取方式。通常的用途是控制 共享信息的更新方式,或用於事務隔離。在控制多個實體並行訪問共同資源方面,文件鎖定是必不 可少的。數據庫等複雜應用嚴重信賴於文件鎖定。

“文件鎖定”從字面上看有鎖定整個文件的意思(通常的確是那樣),但鎖定往往可以發生在更 爲細微的層面,鎖定區域往往可以細緻到單個字節。鎖定與特定文件相關,開始於文件的某個特定 字節地址,包含特定數量的連續字節。這對於協調多個進程互不影響地訪問文件不同區域,是至關 重要的。

文件鎖定有兩種方式:共享的和獨佔的。多個共享鎖可同時對同一文件區域發生作用;獨佔鎖 則不同,它要求相關區域不能有其他鎖定在起作用。

共享鎖和獨佔鎖的經典應用,是控制最初用於讀取的共享文件的更新。某個進程要讀取文件, 會先取得該文件或該文件部分區域的共享鎖。第二個希望讀取相同文件區域的進程也會請求共享 鎖。兩個進程可以並行讀取,互不影響。但是, 假如有第三個進程要更新該文件,它會請求獨佔 鎖。該進程會處於阻滯狀態,直到既有鎖定(共享的、獨佔的)全部解除。一旦給予獨佔鎖,其他 共享鎖的讀取進程會處於阻滯狀態,直到獨佔鎖解除。這樣,更新進程可以更改文件,而其他讀取 進程不會因爲文件的更改得到前後不一致的結果。圖 1-7 和圖 1-8 描述了這一過程。

 

 

文件鎖有建議使用和強制使用之分。建議型文件鎖會向提出請求的進程提供當前鎖定信息,但 操作系統並不要求一定這樣做,而是由相關進程進行協調並關注鎖定信息。多數 Unix 和類 Unix 操 作系統使用建議型鎖,有些也使用強制型鎖或兼而有之。

強制型鎖由操作系統或文件系統強行實施,不管進程對鎖的存在知道與否,都會阻止其對文件 鎖定區域的訪問。微軟的操作系統往往使用的是強制型鎖。假定所有文件鎖均爲建議型,並在訪問 共同資源的各個應用程序間使用一致的文件鎖定,是明智之舉,也是唯一可行的跨平臺策略。依賴 於強制文件鎖定的應用程序,從根子上講就是不可移植的。

摘自JAVA NIO(中文版)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章