頁面緩存的那些事兒

 

提到文件,操做系統必須解決兩個重要的問題。首先是硬盤驅動器的存取速度緩慢得使人頭疼(相對於內存而言),尤爲是磁盤的尋道性能。第二個是要知足‘一次性加載文件內容到物理內存並在程序間共享’的需求。若是你使用進程瀏覽器翻看Windows進程,就會發現大約15MB的共享DLL被加載進了每個進程。我目前的Windows系統就運行了100個進程,若是沒有共享機制,那將消耗大約1.5GB的物理內存僅僅用於存放公用DLL。這可不怎麼好。一樣的,幾乎全部的Linux程序都須要ld.so和libc,以及其它的公用函數庫。html

使人愉快的是,這兩個問題能夠被一石二鳥的解決:頁面緩存(page cache),內核用它保存與頁面同等大小的文件數據塊。爲了展現頁面緩存,我須要祭出一個名叫render的Linux程序,它會打開一個scene.dat文件,每次讀取其中的512字節,並將這些內容保存到一個創建在堆上的內存塊中。首次的讀取是這樣的:linux

wps_clip_image-20163

在讀取了12KB之後,render的堆以及相關的頁幀狀況以下:程序員

wps_clip_image-29015

這看起來很簡單,但還有不少事情會發生。首先,即便這個程序只調用了常規的read函數,此時也會有三個4KB的頁幀存儲在頁面緩存當中,它們持有scene.dat的一部分數據。儘管有時這使人驚訝,但的確全部的常規文件I/O都是經過頁面緩存來進行的。在x86 Linux裏,內核將文件看做是4KB大小的數據塊的序列。即便你只從文件讀取一個字節,包含此字節的整個4KB數據塊都會被讀取,並放入到頁面緩存當中。這樣作是有道理的,由於磁盤的持續性數據吞吐量很不錯,並且通常說來,程序對於文件中某區域的讀取都不止幾個字節。頁面緩存知道每個4KB數據塊在文件中的對應位置,如上圖所示的#0, #1等等。與Linux的頁面緩存相似,Windows使用256KB的views。數據庫

不幸的是,在一個普通的文件讀取操做中,內核必須複製頁面緩存的內容到一個用戶緩衝區中,這不只消耗CPU時間,傷害了CPU cache的性能,還由於存儲了重複信息而浪費物理內存。如上面每張圖所示,scene.dat的內容被保存了兩遍,並且程序的每一個實例都會保存一份。至此,咱們緩和了磁盤延遲的問題,但卻在其他的每一個問題上慘敗。內存映射文件(memory-mapped files)將引領咱們走出混亂:編程

wps_clip_image-27444

當你使用文件映射的時候,內核將你的程序的虛擬內存頁直接映射到頁面緩存上。這將致使一個顯著的性能提高:《Windows系統編程》指出常規的文件讀取操做運行時性能改善30%以上;《Unix環境高級編程》指出相似的狀況也發生在Linux和Solaris系統上。你還可能所以而節省下大量的物理內存,這依賴於你的程序的具體狀況。c#

和之前同樣,提到性能,實際測量纔是王道,可是內存映射的確值得被程序員們放入工具箱。相關的API也很漂亮,它提供了像訪問內存中的字節同樣的方式來訪問一個文件,不須要你多操心,也不犧牲代碼的可讀性。回憶一下地址空間、還有那個在Unix類系統上關於mmap的實驗,Windows下的CreateFileMapping及其在高級語言中的各類可用封裝。當你映射一個文件時,它的內容並非馬上就被所有放入內存的,而是依賴頁故障(page fault)按需讀取。在獲取了一個包含所需的文件數據的頁幀後,對應的故障處理函數會將你的虛擬內存頁映射到頁面緩存上。若是所需內容不在緩存當中,此過程還將包含磁盤I/O操做。瀏覽器

如今給你出一個流行的測試題。想象一下,在最後一個render程序的實例退出之時,那些保存了scene.dat的頁面緩存會被馬上清理嗎?人們一般會這樣認爲,但這是個壞主意。若是你仔細想一想,咱們常常會在一個程序中建立一個文件,退出,緊接着在第二個程序中使用這個文件。頁面緩存必須能處理此類狀況。若是你再多想一想,內核何須老是要捨棄頁面緩存中的內容呢?記住,磁盤比RAM慢5個數量級,所以一個頁面緩存的命中(hit)就意味着巨大的勝利。只要還有足夠的空閒物理內存,緩存就應該儘量保持滿狀態。因此它與特定的進程並不相關,而是一個系統級的資源。若是你一週前運行過render,而此時scene.dat還在緩存當中,那真使人高興。這就是爲何內核緩存的大小會穩步增長,直到緩存上限。這並不是由於操做系統是破爛貨,吞噬你的RAM,事實上這是種好的行爲,反而釋放物理內存纔是一種浪費。緩存要利用得越充分越好緩存

因爲使用了頁面緩存體系結構,當一個程序調用write()時,相關的字節被簡單的複製到頁面緩存中,而且將頁面標記爲髒的(dirty)。磁盤I/O通常不會馬上發生,所以你的程序的執行不會被打斷去等待磁盤設備。這樣作的缺點是,若是此時計算機死機,那麼你寫入的數據將不會被記錄下來。所以重要的文件,好比數據庫事務記錄必須被fsync() (可是還要當心磁盤控制器的緩存)。另外一方面,讀取操做通常會打斷你的程序直到準備好所需的數據。內核一般採用積極加載(eager loading)的方式來緩解這個問題。以提早讀取(read ahead)爲例,內核會預先加載一些頁到頁面緩存,並期待你的讀取操做。經過提示系統即將對文件進行的是順序仍是隨機讀取操做(參看madvise(), readahead(), Windows緩存提示),你能夠幫助內核調整它的積極加載行爲。Linux的確會對內存映射文件進行預取,但我不太肯定Windows是否也如此。最後須要一提的是,你還能夠經過在Linux中使用O_DIRECT或在Windows中使用NO_BUFFERING來繞過頁面緩存,有些數據庫軟件就是這麼作的。app

一個文件映射能夠是私有的(private)或共享的(shared)。這裏的區別只有在更改(update)內存中的內容時纔會顯現出來:在私有映射中,更改並不會被提交到磁盤或對其餘進程可見,而這在共享的映射中就會發生。內核使用寫時拷貝(copy on write)技術,經過頁表項(page table entries),實現私有映射。在下面的例子中,render和另外一個叫render3d的程序(我是否是頗有創意?)同時私有映射了scene.dat。隨後render改寫了映射到此文件的虛擬內存區域:函數

wps_clip_image-7873

上圖所示的只讀的頁表項並不意味着映射是隻讀的,它們只是內核耍的小把戲,用於共享物理內存直到可能的最後一刻。你會發現‘私有’一詞是多麼的不恰當,你只需記住它只在數據發生更改時起做用。此設計所帶來的一個結果就是,一個以私有方式映射文件的虛擬內存頁能夠觀察到其餘進程對此文件的改動,只要以前對這個內存頁進行的都是讀取操做。一旦發生過寫時拷貝,就不會再觀察到其餘進程對此文件的改動了。此行爲不是內核提供的,而是在x86系統上就會如此。另外,從API的角度來講,這也是合理的。與此相反,共享映射只是簡單的映射到頁面緩存,僅此而已。對頁面的全部更改操做對其餘進程均可見,並且最終會執行磁盤操做。最後,若是此共享映射是隻讀的,那麼頁故障將觸發段錯誤(segmentation fault)而不是寫時拷貝。

被動態加載的函數庫經過文件映射機制放入到你的程序的地址空間中。這裏沒有任何特別之處,一樣是採用私有文件映射,跟提供給你調用的常規API別無二致。下面的例子展現了兩個運行中的render程序的一部分地址空間,還有物理內存。它將咱們以前看到的概念都聯繫在了一塊兒。

wps_clip_image-3801

相關文章
相關標籤/搜索