Linux中國

頁面緩存:內存和文件之間的那些事

上一篇文章中我們學習了內核怎麼為一個用戶進程 管理虛擬內存,而沒有提及文件和 I/O。這一篇文章我們將專門去講這個重要的主題 —— 頁面緩存。文件和內存之間的關係常常很不好去理解,而它們對系統性能的影響卻是非常大的。

在面對文件時,有兩個很重要的問題需要操作系統去解決。第一個是相對內存而言,慢的讓人發狂的硬碟驅動器,尤其是磁碟尋道。第二個是需要將文件內容一次性地載入到物理內存中,以便程序間共享文件內容。如果你在 Windows 中使用 進程瀏覽器 去查看它的進程,你將會看到每個進程中載入了大約 ~15MB 的公共 DLL。我的 Windows 機器上現在大約運行著 100 個進程,因此,如果不共享的話,僅這些公共的 DLL 就要使用高達 ~1.5 GB 的物理內存。如果是那樣的話,那就太糟糕了。同樣的,幾乎所有的 Linux 進程都需要 ld.so 和 libc,加上其它的公共庫,它們佔用的內存數量也不是一個小數目。

幸運的是,這兩個問題都用一個辦法解決了:頁面緩存 —— 保存在內存中的頁面大小的文件塊。為了用圖去說明頁面緩存,我捏造出一個名為 render 的 Linux 程序,它打開了文件 scene.dat,並且一次讀取 512 位元組,並將文件內容存儲到一個分配到堆中的塊上。第一次讀取的過程如下:

Reading and the page cache

  1. render 請求 scene.dat 從位移 0 開始的 512 位元組。
  2. 內核搜尋頁面緩存中 scene.dat 的 4kb 塊,以滿足該請求。假設該數據沒有緩存。
  3. 內核分配頁面幀,初始化 I/O 請求,將 scend.dat 從位移 0 開始的 4kb 複製到分配的頁面幀。
  4. 內核從頁面緩存複製請求的 512 位元組到用戶緩衝區,系統調用 read() 結束。

讀取完 12KB 的文件內容以後,render 程序的堆和相關的頁面幀如下圖所示:

Non-mapped file read

它看起來很簡單,其實這一過程做了很多的事情。首先,雖然這個程序使用了普通的讀取(read)調用,但是,已經有三個 4KB 的頁面幀將文件 scene.dat 的一部分內容保存在了頁面緩存中。雖然有時讓人覺得很驚奇,但是,普通的文件 I/O 就是這樣通過頁面緩存來進行的。在 x86 架構的 Linux 中,內核將文件認為是一系列的 4KB 大小的塊。如果你從文件中讀取單個位元組,包含這個位元組的整個 4KB 塊將被從磁碟中讀入到頁面緩存中。這是可以理解的,因為磁碟通常是持續吞吐的,並且程序一般也不會從磁碟區域僅僅讀取幾個位元組。頁面緩存知道文件中的每個 4KB 塊的位置,在上圖中用 #0#1 等等來描述。Windows 使用 256KB 大小的 視圖 view ,類似於 Linux 的頁面緩存中的 頁面 page

不幸的是,在一個普通的文件讀取中,內核必須拷貝頁面緩存中的內容到用戶緩衝區中,它不僅花費 CPU 時間和影響 CPU 緩存在複製數據時也浪費物理內存。如前面的圖示,scene.dat 的內存被存儲了兩次,並且,程序中的每個實例都用另外的時間去存儲內容。我們雖然解決了從磁碟中讀取文件緩慢的問題,但是在其它的方面帶來了更痛苦的問題。內存映射文件是解決這種痛苦的一個方法:

Mapped file read

當你使用文件映射時,內核直接在頁面緩存上映射你的程序的虛擬頁面。這樣可以顯著提升性能:Windows 系統編程 報告指出,在相關的普通文件讀取上運行時性能提升多達 30% ,在 Unix 環境中的高級編程 的報告中,文件映射在 Linux 和 Solaris 也有類似的效果。這取決於你的應用程序類型的不同,通過使用文件映射,可以節約大量的物理內存。

對高性能的追求是永恆不變的目標,測量是很重要的事情,內存映射應該是程序員始終要使用的工具。這個 API 提供了非常好用的實現方式,它允許你在內存中按位元組去訪問一個文件,而不需要為了這種好處而犧牲代碼可讀性。在一個類 Unix 的系統中,可以使用 mmap 查看你的 地址空間,在 Windows 中,可以使用 CreateFileMapping,或者在高級編程語言中還有更多的可用封裝。當你映射一個文件內容時,它並不是一次性將全部內容都映射到內存中,而是通過 頁面故障 來按需映射的。在 獲取 需要的文件內容的頁面幀後,頁面故障句柄 映射你的虛擬頁面 到頁面緩存上。如果一開始文件內容沒有緩存,這還將涉及到磁碟 I/O。

現在出現一個突發的狀況,假設我們的 render 程序的最後一個實例退出了。在頁面緩存中保存著 scene.dat 內容的頁面要立刻釋放掉嗎?人們通常會如此考慮,但是,那樣做並不是個好主意。你應該想到,我們經常在一個程序中創建一個文件,退出程序,然後,在第二個程序去使用這個文件。頁面緩存正好可以處理這種情況。如果考慮更多的情況,內核為什麼要清除頁面緩存的內容?請記住,磁碟讀取的速度要慢於內存 5 個數量級,因此,命中一個頁面緩存是一件有非常大收益的事情。因此,只要有足夠大的物理內存,緩存就應該保持全滿。並且,這一原則適用於所有的進程。如果你現在運行 render 一周後, scene.dat 的內容還在緩存中,那麼應該恭喜你!這就是什麼內核緩存越來越大,直至達到最大限制的原因。它並不是因為操作系統設計的太「垃圾」而浪費你的內存,其實這是一個非常好的行為,因為,釋放物理內存才是一種「浪費」。(LCTT 譯註:釋放物理內存會導致頁面緩存被清除,下次運行程序需要的相關數據,需要再次從磁碟上進行讀取,會「浪費」 CPU 和 I/O 資源)最好的做法是儘可能多的使用緩存。

由於頁面緩存架構的原因,當程序調用 write() 時,位元組只是被簡單地拷貝到頁面緩存中,並將這個頁面標記為「臟」頁面。磁碟 I/O 通常並不會立即發生,因此,你的程序並不會被阻塞在等待磁碟寫入上。副作用是,如果這時候發生了電腦死機,你的寫入將不會完成,因此,對於至關重要的文件,像資料庫事務日誌,要求必須進行 fsync()(仍然還需要去擔心磁碟控制器的緩存失敗問題),另一方面,讀取將被你的程序阻塞,直到數據可用為止。內核採取預載入的方式來緩解這個矛盾,它一般提前預讀取幾個頁面並將它載入到頁面緩存中,以備你後來的讀取。在你計划進行一個順序或者隨機讀取時(請查看 madvise()readahead()Windows 緩存提示 ),你可以通過 提示 hint 幫助內核去調整這個預載入行為。Linux 會對內存映射的文件進行 預讀取,但是我不確定 Windows 的行為。當然,在 Linux 中它可能會使用 O_DIRECT 跳過預讀取,或者,在 Windows 中使用 NO_BUFFERING 去跳過預讀,一些資料庫軟體就經常這麼做。

一個文件映射可以是私有的,也可以是共享的。當然,這只是針對內存中內容的更新而言:在一個私有的內存映射上,更新並不會提交到磁碟或者被其它進程可見,然而,共享的內存映射,則正好相反,它的任何更新都會提交到磁碟上,並且對其它的進程可見。內核使用 寫時複製 copy on write (CoW)機制,這是通過 頁面表條目 page table entry (PTE)來實現這種私有的映射。在下面的例子中,render 和另一個被稱為 render3d 的程序都私有映射到 scene.dat 上。然後 render 去寫入映射的文件的虛擬內存區域:

The Copy-On-Write mechanism

  1. 兩個程序私有地映射 scene.dat,內核誤導它們並將它們映射到頁面緩存,但是使該頁面表條目只讀。
  2. render 試圖寫入到映射 scene.dat 的虛擬頁面,處理器發生頁面故障。
  3. 內核分配頁面幀,複製 scene.dat 的第二塊內容到其中,並映射故障的頁面到新的頁面幀。
  4. 繼續執行。程序就當做什麼都沒發生。

上面展示的只讀頁面表條目並不意味著映射是只讀的,它只是內核的一個用於共享物理內存的技巧,直到儘可能的最後一刻之前。你可以認為「私有」一詞用的有點不太恰當,你只需要記住,這個「私有」僅用於更新的情況。這種設計的重要性在於,要想看到被映射的文件的變化,其它程序只能讀取它的虛擬頁面。一旦「寫時複製」發生,從其它地方是看不到這種變化的。但是,內核並不能保證這種行為,因為它是在 x86 中實現的,從 API 的角度來看,這是有意義的。相比之下,一個共享的映射只是將它簡單地映射到頁面緩存上。更新會被所有的進程看到並被寫入到磁碟上。最終,如果上面的映射是只讀的,頁面故障將觸發一個內存段失敗而不是寫到一個副本。

動態載入庫是通過文件映射融入到你的程序的地址空間中的。這沒有什麼可奇怪的,它通過普通的 API 為你提供與私有文件映射相同的效果。下面的示例展示了映射文件的 render 程序的兩個實例運行的地址空間的一部分,以及物理內存,嘗試將我們看到的許多概念綜合到一起。

Mapping virtual memory to physical memory

這是內存架構系列的第三部分的結論。我希望這個系列文章對你有幫助,對理解操作系統的這些主題提供一個很好的思維模型。

via:https://manybutfinite.com/post/page-cache-the-affair-between-memory-and-files/

作者:Gustavo Duarte 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國