1. 程式人生 > >簡述Linux虛擬記憶體管理

簡述Linux虛擬記憶體管理

原文地址:https://cloud.tencent.com/ developer/article/1157420

虛擬儲存

虛擬儲存(virtual memory, VM)的基本思想是: 維護一個虛擬的邏輯記憶體機制(通常比實體記憶體大得多), 程序都基於這個虛擬記憶體, 在程序執行時動態的將虛擬記憶體地址對映到實際的實體記憶體.

VM的設計體現了軟體工程思想: 封裝, 抽象, 依賴倒置, 非常棒. 每個執行中的程序無需再去關心實際實體記憶體是多大, 分配記憶體會不會超出限制, 哪些記憶體已經被其他程序佔用等等, 這些都交由kernel的記憶體管理單元來解決, 暴露給程序的"介面"只有每個程序獨有的虛擬地址空間.

如上圖所示: 程式中產生的記憶體地址成為虛擬地址(virtual address), 又稱為邏輯地址(logic address), 邏輯地址被送到記憶體管理單元(memory manager unit, MMU), 對映成實體記憶體地址之後, 再送到記憶體總線上.

分頁

MMU的主要職責就是將邏輯地址, 轉成實體地址. 以32位Linux為例, MMU可以當成一個數學函式: f(x) = y, 輸入x是一個0-4G範圍內的邏輯地址, 輸出y是實際的實體地址.

最簡單暴力的方法, 莫過於直接建一層對映關係, 不過這樣對映表就得4G大小了……

因為實際上我們並不需要把所有的對映關係都建立起來, 而只需要為用到的記憶體做對映, 所以, 前輩們用了分頁的方法來解決這個問題(實際是一個多階雜湊).

一個典型的二級頁表來處理分頁: 32位的邏輯地址被分成了3段, 10位的一級頁表索引(page table 1 index), 10位的二級頁表索引(page table 2 index)和剩下的12位頁面偏移量(page offset). 所謂頁面(page), 是現在大部分MMU中用來管理記憶體的單位, Linux下常見的page大小是4k(12位的偏移量剛好是一個page, 即4k).

對於一個程序而言, 它需要用到的頁表: 一級頁表,以及部分用到的二級頁表(不需要全部的). 以一個佔用16M記憶體地址空間的程序為例, 理論上它只需要1個一級頁表, 和4個二級頁表, 頁表開銷即 5 * 4k = 20k, 能節省大量的頁表開銷.

在多級頁表中, 頁表分級越多, 越靈活, 但是帶來的時間成本也就越高, 複雜度也越高. 二級, 或者三級頁表是一個比較合理的選擇. 為了相容不同的CPU, Linux 2.6.11 之後使用了四級分頁機制, 在不同的CPU環境下可以靈活擴充套件成二級或者三級.

邏輯地址對映成實體地址的過程是通過MMU硬體來完成的. 除此之外, 還有一個TLB的硬體, translation lookaside buffer, 即頁表緩衝, 它是一塊高速cache, 通過CR3暫存器來重新整理, 能加速虛擬記憶體定址的過程.

頁面置換

程序中用到的程式碼段, 資料段和堆疊的總大小可能超過可用的實體記憶體總數, VM提供了一種機制來解決這個問題: 把當前使用的那一部分放到記憶體中, 其他部分儲存在磁碟上, 並在需要時在磁碟和記憶體中做交換. 這就是頁面置換.

當一個邏輯地址, 經過MMU對映後發現, 對應的頁表項還沒有對映到實體記憶體, 就會觸發缺頁錯誤(page fault): CPU 需要陷入 kernel, 找到一個可用的實體記憶體頁面, 從頁表項對映過去. 如果這個時候沒有空閒的實體記憶體頁面, 就需要做頁面置換了, 作業系統通過某些演算法, 從實體記憶體中選一個當前在用的頁面, (是否需要寫到磁碟, 取決於有沒有被修改過), 重新調入, 建立頁表項到之的對映關係.

分段

分段的思想, 說穿了就是把記憶體分成若干段, 每個段是一個單獨的地址空間, 有自己的起始的基地址, 根據 基地址+偏移量 來做定址.

分段的好處是帶來了比較大的靈活性, 也更安全. 每個段都構成了自己的獨立地址空間, 增大或者減小而不會影響其他段. 還可以對每個段設定不同的保護級別.

Linux下采用的是段頁式記憶體管理, 先分段, 再分頁. 但是因為Linux中所有的段基址都設定成了0, 段偏移量相當於就是線性地址, 只用了一個地址空間, 效果上就是正常的分頁. 這麼做的原因是為了相容各種硬體體系.

雖然Linux下, "分段"只是一個擺設, 但是在程序的記憶體管理中, 還是應用了分段的思想的: 每一個程序在執行時, 它的邏輯地址空間都會被分為程式碼段, 資料段, 堆, 棧等, 當訪問段之外的記憶體地址時, kernel 能監測到並給出段錯誤(segment fault).

VM管理

Linux kernel 主要提供了兩種記憶體分配演算法: buddy 和 slab, 結合使用。buddy 提供了2的冪大小記憶體塊的分配方法,具有陣列特性,簡單高效, 但是缺點在於記憶體碎片。slab 提供了小物件的記憶體分配方法, 實際上是一個多級快取列表, 最小的分配單位稱為一個slab(一個或者多個連續頁), 被分配為多個物件來使用.

kswapd 是一個 daemon 程序, 對系統記憶體做定時檢查, 一般是1秒一次. 如果發現沒有足夠的空閒頁面, 就做頁回收(page reclaiming), 將不再使用的頁面換出. 如果要換出的頁面髒了, 往往還需要寫回到磁碟或者swap.

bdflush 也是 daemon 程序, 週期性的檢查髒緩衝(磁碟cache), 並寫回磁碟. 不過在 Linux 2.6 之後, pdflush 取代了 bdflush, 前者的優勢在於: 可以開多個執行緒, 而 bdflush 只能是單執行緒, 這就保證了不會在回寫繁忙時阻塞; 另外, bdflush 的操作物件是緩衝, 而 pdflush 是基於頁面的, 顯然 pdflush 的效率會更高.

觀察記憶體

"pmap –x pid" 這個命令, 能將/proc/pid/maps中的資料, 以更人性化的方式展示出來:

從上圖可以看到, 每一項內容都清晰的標出了物件, 記憶體起始地址(邏輯地址), 佔用的記憶體大小, 實際分配的記憶體(RSS, 也就是常駐記憶體), 以及髒記憶體, 這些單位都是kb, 並給出了最終的統計結果. 統計結果的前兩項就是 top 中顯示的 VIRT 和 RSS.

VM tuning

這裡就只關注 Linux 2.6 之後的情況了(2.4之前諸如 bdflush 就不在討論範圍之內). 所有的VM可以調整的引數項, 都在/proc/sys/vm目錄下:

可以"sysctl vm.param"觀察引數的值, "sysctl –w vm.param=value"來修改引數.具體的每一項引數的含義可以參考: https://www.kernel.org/doc/Documentation/sysctl/vm.txt.

  1. pdflush調優, 其實這一塊跟磁碟IO關係比較緊.

  • dirty_writeback_centisecs, 預設是500, 單位是毫秒. 意思是每5秒喚醒 pdflush (多個執行緒), 將髒頁面寫回磁碟. 把這個引數調低可以增加 pdflush 被喚醒的頻率, 不過在核心實現中, pdflush 在需要的時候會自動被喚醒, 所以這個引數的效果不可預期.

  • dirty_expire_centiseconds, 預設是3000, 單位是毫秒, 是指髒頁面的過期時間, 超過了這個時間, 就會觸發 pdflush 做回寫.

  • dirty_background_ratio, 預設是10, 是指總記憶體中髒頁面的百分比. 低於這個閾值時, pdflush 才會停止做回寫, 有的核心版本的預設值是5.

  • dirty_ratio, 這也是一個百分比, 預設40, 是總記憶體中髒頁面的百分比. 超過這個閾值, 就一定等待 pdflush 向磁盤迴寫. 與 dirty_background_ratio 的區別在與: 如果 cache 的增長超過了 pdflush 的回寫速率時, 有可能 pdflush 來不及回寫, 在超過40\%這個閾值時, 程序就會等待, 直到 pdflush 處理到這個閾值之下. 此時就是一個IO瓶頸.

IO比較重的時候, 可以考慮的調優手段: 首先嚐試調低 dirty_background_ratio, 其次是 dirty_background_ratio, 然後是 dirty_expire_centiseconds, dirty_writeback_centisecs 這一項可以不用考慮.

  1. swapness, 這個表示了 swap 分割槽的使用程度, 等於0時表示儘可能不用 swap, 等於100表示積極的使用 swap, 預設是60. 這個引數取決於具體的需求.

  2. drop_caches, 這個跟cache有關, 預設是0. 設定不同的引數可以回收系統的 cache 和 buffers, 不過略顯粗暴(cache 和 buffer 的存在是有意義的).

  • free pagecache: sysctl -w vm.drop_caches=1

  • free dentries and inodes: sysctl -w vm.drop_caches=2

  • free pagecache, dentries and inodes: sysctl -w vm.drop_caches=3