1. 程式人生 > >Linux的快取記憶體(cache memory)

Linux的快取記憶體(cache memory)

PS:為什麼Linux系統沒執行多少程式,顯示的可用記憶體這麼少?其實Linux與Win的記憶體管理不同,會盡量快取記憶體以提高讀寫效能,通常叫做Cache Memory。

為什麼Linux系統沒執行多少程式,顯示的可用記憶體這麼少?其實Linux與Win的記憶體管理不同,會盡量快取記憶體以提高讀寫效能,通常叫做Cache Memory。

有時候你會發現沒有什麼程式在執行,但是使用top或free命令看到可用記憶體free項會很少,此時檢視系統的 /proc/meminfo 檔案,會發現有一項 Cached Memory:
輸入cat /proc/meminfo檢視:

MemTotal: 16425996 kB
    MemFree: 5698808 kB
    Buffers: 380904 kB
    Cached: 9389356 kB
    SwapCached: 212 kB
    Active: 6569200 kB
    Inactive: 3725364 kB
    HighTotal: 0 kB
    HighFree: 0 kB
    LowTotal: 16425996 kB
    LowFree: 5698808 kB
    SwapTotal: 8273464 kB
    SwapFree: 8273252 kB
    Dirty: 980 kB
    Writeback: 0 kB
    AnonPages: 524108 kB
    Mapped: 24568 kB
    Slab: 381776 kB
    PageTables: 7496 kB
    NFS_Unstable: 0 kB
    Bounce: 0 kB
    CommitLimit: 16486460 kB
    Committed_AS: 2143856 kB
    VmallocTotal: 34359738367 kB
    VmallocUsed: 267656 kB
    VmallocChunk: 34359469303 kB
    HugePages_Total: 0
    HugePages_Free: 0
    HugePages_Rsvd: 0
    Hugepagesize: 2048 kB

free命令裡各項記憶體指標說明:

total used free shared buffers cached
Mem: 16425996 10727220 5698776 0 380904 9389832
-/+ buffers/cache: 956484 15469512
Swap: 8273464 212 8273252

其中第一行用全域性角度描述系統使用的記憶體狀況:
total——總實體記憶體
used——已使用記憶體,一般情況這個值會比較大,因為這個值包括了cache+應用程式使用的記憶體
free——完全未被使用的記憶體
shared——應用程式共享記憶體
buffers——快取,主要用於目錄方面,inode值等(ls大目錄可看到這個值增加)
cached——快取,用於已開啟的檔案
總結:
total=used+free
used=buffers+cached (maybe add shared also)

第二行描述應用程式的記憶體使用:
前個值表示-buffers/cache——應用程式使用的記憶體大小,used減去快取值
後個值表示+buffers/cache——所有可供應用程式使用的記憶體大小,free加上快取值
總結:
-buffers/cache=used-buffers-cached
+buffers/cache=free+buffers+cached

第三行表示swap的使用:
used——已使用
free——未使用

什麼是Cache Memory(快取記憶體):

當你讀寫檔案的時候,Linux核心為了提高讀寫效能與速度,會將檔案在記憶體中進行快取,這部分記憶體就是Cache Memory(快取記憶體)。即使你的程式執行結束後,Cache Memory也不會自動釋放。這就會導致你在Linux系統中程式頻繁讀寫檔案後,你會發現可用實體記憶體會很少。

其實這快取記憶體(Cache Memory)在你需要使用記憶體的時候會自動釋放,所以你不必擔心沒有記憶體可用。如果你希望手動去釋放Cache Memory也是有辦法的。

如何釋放Cache Memory(快取記憶體):

用下面的命令可以釋放Cache Memory:

To free pagecache:
echo 1 > /proc/sys/vm/drop_caches
To free dentries and inodes:
echo 2 > /proc/sys/vm/drop_caches
To free pagecache, dentries and inodes:
echo 3 > /proc/sys/vm/drop_caches

注意,釋放前最好sync一下,防止丟失資料。

總結:個人經驗認為沒必要手動釋放,這種記憶體管理方式也是比win優勝的地方之一!因為Linux的核心記憶體管理機制,一般情況下不需要特意去釋放已經使用的cache。這些cache起來的內容可以提高檔案以及磁碟的讀寫速度。

頁面快取——記憶體與檔案的那些事兒

提到檔案,作業系統必須解決兩個重要的問題。首先是硬碟驅動器的存取速度緩慢得令人頭疼(相對於記憶體而言),尤其是磁碟的尋道效能。第二個是要滿足‘一次性載入檔案內容到實體記憶體並在程式間共享’的需求。如果你使用程序瀏覽器翻看Windows程序,就會發現大約15MB的共享DLL被載入進了每一個程序。我目前的Windows系統就運行了100個程序,如果沒有共享機制,那將消耗大約1.5GB的實體記憶體僅僅用於存放公用DLL。這可不怎麼好。同樣的,幾乎所有的Linux程式都需要ld.so和libc,以及其它的公用函式庫。

令人愉快的是,這兩個問題可以被一石二鳥的解決:頁面快取(page cache),核心用它儲存與頁面同等大小的檔案資料塊。為了展示頁面快取,我需要祭出一個名叫render的Linux程式,它會開啟一個scene.dat檔案,每次讀取其中的512位元組,並將這些內容儲存到一個建立在堆上的記憶體塊中。首次的讀取是這樣的:
這裡寫圖片描述
在讀取了12KB以後,render的堆以及相關的頁幀情況如下:
這裡寫圖片描述
這看起來很簡單,但還有很多事情會發生。首先,即使這個程式只調用了常規的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)將引領我們走出混亂:
這裡寫圖片描述
當你使用檔案對映的時候,核心將你的程式的虛擬記憶體頁直接對映到頁面快取上。這將導致一個顯著的效能提升:《Windows系統程式設計》指出常規的檔案讀取操作執行時效能改善30%以上;《Unix環境高階程式設計》指出類似的情況也發生在Linux和Solaris系統上。你還可能因此而節省下大量的實體記憶體,這依賴於你的程式的具體情況。

和以前一樣,提到效能,實際測量才是王道,但是記憶體對映的確值得被程式設計師們放入工具箱。相關的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來繞過頁面快取,有些資料庫軟體就是這麼做的。

一個檔案對映可以是私有的(private)或共享的(shared)。這裡的區別只有在更改(update)記憶體中的內容時才會顯現出來:在私有對映中,更改並不會被提交到磁碟或對其他程序可見,而這在共享的對映中就會發生。核心使用寫時拷貝(copy on write)技術,通過頁表項(page table entries),實現私有對映。在下面的例子中,render和另一個叫render3d的程式(我是不是很有創意?)同時私有映射了scene.dat。隨後render改寫了對映到此檔案的虛擬記憶體區域:
這裡寫圖片描述
上圖所示的只讀的頁表項並不意味著對映是隻讀的,它們只是核心耍的小把戲,用於共享實體記憶體直到可能的最後一刻。你會發現‘私有’一詞是多麼的不恰當,你只需記住它只在資料發生更改時起作用。此設計所帶來的一個結果就是,一個以私有方式對映檔案的虛擬記憶體頁可以觀察到其他程序對此檔案的改動,只要之前對這個記憶體頁進行的都是讀取操作。一旦發生過寫時拷貝,就不會再觀察到其他程序對此檔案的改動了。此行為不是核心提供的,而是在x86系統上就會如此。另外,從API的角度來說,這也是合理的。與此相反,共享對映只是簡單的對映到頁面快取,僅此而已。對頁面的所有更改操作對其他程序都可見,而且最終會執行磁碟操作。最後,如果此共享對映是隻讀的,那麼頁故障將觸發段錯誤(segmentation fault)而不是寫時拷貝。

被動態載入的函式庫通過檔案對映機制放入到你的程式的地址空間中。這裡沒有任何特別之處,同樣是採用私有檔案對映,跟提供給你呼叫的常規API別無二致。下面的例子展示了兩個執行中的render程式的一部分地址空間,還有實體記憶體。它將我們之前看到的概念都聯絡在了一起。

這裡寫圖片描述