原文地址:https://cloud.tencent.com/ developer/article/1157420
虛擬存儲
虛擬存儲(virtual memory, VM)的基本思想是: 維護一個虛擬的邏輯內(nèi)存機制(通常比物理內(nèi)存大得多), 進程都基于這個虛擬內(nèi)存, 在進程運行時動態(tài)的將虛擬內(nèi)存地址映射到實際的物理內(nèi)存.
VM的設(shè)計體現(xiàn)了軟件工程思想: 封裝, 抽象, 依賴倒置, 非常棒. 每個運行中的進程無需再去關(guān)心實際物理內(nèi)存是多大, 分配內(nèi)存會不會超出限制, 哪些內(nèi)存已經(jīng)被其他進程占用等等, 這些都交由kernel的內(nèi)存管理單元來解決, 暴露給進程的"接口"只有每個進程獨有的虛擬地址空間.

如上圖所示: 程序中產(chǎn)生的內(nèi)存地址成為虛擬地址(virtual address), 又稱為邏輯地址(logic address), 邏輯地址被送到內(nèi)存管理單元(memory manager unit, MMU), 映射成物理內(nèi)存地址之后, 再送到內(nèi)存總線上.
分頁
MMU的主要職責(zé)就是將邏輯地址, 轉(zhuǎn)成物理地址. 以32位Linux為例, MMU可以當(dāng)成一個數(shù)學(xué)函數(shù): f(x) = y, 輸入x是一個0-4G范圍內(nèi)的邏輯地址, 輸出y是實際的物理地址.
最簡單暴力的方法, 莫過于直接建一層映射關(guān)系, 不過這樣映射表就得4G大小了……
因為實際上我們并不需要把所有的映射關(guān)系都建立起來, 而只需要為用到的內(nèi)存做映射, 所以, 前輩們用了分頁的方法來解決這個問題(實際是一個多階哈希).
一個典型的二級頁表來處理分頁: 32位的邏輯地址被分成了3段, 10位的一級頁表索引(page table 1 index), 10位的二級頁表索引(page table 2 index)和剩下的12位頁面偏移量(page offset). 所謂頁面(page), 是現(xiàn)在大部分MMU中用來管理內(nèi)存的單位, Linux下常見的page大小是4k(12位的偏移量剛好是一個page, 即4k).

對于一個進程而言, 它需要用到的頁表: 一級頁表,以及部分用到的二級頁表(不需要全部的). 以一個占用16M內(nèi)存地址空間的進程為例, 理論上它只需要1個一級頁表, 和4個二級頁表, 頁表開銷即 5 * 4k = 20k, 能節(jié)省大量的頁表開銷.
在多級頁表中, 頁表分級越多, 越靈活, 但是帶來的時間成本也就越高, 復(fù)雜度也越高. 二級, 或者三級頁表是一個比較合理的選擇. 為了兼容不同的CPU, Linux 2.6.11 之后使用了四級分頁機制, 在不同的CPU環(huán)境下可以靈活擴展成二級或者三級.
邏輯地址映射成物理地址的過程是通過MMU硬件來完成的. 除此之外, 還有一個TLB的硬件, translation lookaside buffer, 即頁表緩沖, 它是一塊高速cache, 通過CR3寄存器來刷新, 能加速虛擬內(nèi)存尋址的過程.
頁面置換
進程中用到的代碼段, 數(shù)據(jù)段和堆棧的總大小可能超過可用的物理內(nèi)存總數(shù), VM提供了一種機制來解決這個問題: 把當(dāng)前使用的那一部分放到內(nèi)存中, 其他部分保存在磁盤上, 并在需要時在磁盤和內(nèi)存中做交換. 這就是頁面置換.
當(dāng)一個邏輯地址, 經(jīng)過MMU映射后發(fā)現(xiàn), 對應(yīng)的頁表項還沒有映射到物理內(nèi)存, 就會觸發(fā)缺頁錯誤(page fault): CPU 需要陷入 kernel, 找到一個可用的物理內(nèi)存頁面, 從頁表項映射過去. 如果這個時候沒有空閑的物理內(nèi)存頁面, 就需要做頁面置換了, 操作系統(tǒng)通過某些算法, 從物理內(nèi)存中選一個當(dāng)前在用的頁面, (是否需要寫到磁盤, 取決于有沒有被修改過), 重新調(diào)入, 建立頁表項到之的映射關(guān)系.
分段
分段的思想, 說穿了就是把內(nèi)存分成若干段, 每個段是一個單獨的地址空間, 有自己的起始的基地址, 根據(jù) 基地址+偏移量 來做尋址.
分段的好處是帶來了比較大的靈活性, 也更安全. 每個段都構(gòu)成了自己的獨立地址空間, 增大或者減小而不會影響其他段. 還可以對每個段設(shè)置不同的保護級別.
Linux下采用的是段頁式內(nèi)存管理, 先分段, 再分頁. 但是因為Linux中所有的段基址都設(shè)置成了0, 段偏移量相當(dāng)于就是線性地址, 只用了一個地址空間, 效果上就是正常的分頁. 這么做的原因是為了兼容各種硬件體系.
雖然Linux下, "分段"只是一個擺設(shè), 但是在進程的內(nèi)存管理中, 還是應(yīng)用了分段的思想的: 每一個進程在運行時, 它的邏輯地址空間都會被分為代碼段, 數(shù)據(jù)段, 堆, 棧等, 當(dāng)訪問段之外的內(nèi)存地址時, kernel 能監(jiān)測到并給出段錯誤(segment fault).
VM管理

Linux kernel 主要提供了兩種內(nèi)存分配算法: buddy 和 slab, 結(jié)合使用。buddy 提供了2的冪大小內(nèi)存塊的分配方法,具有數(shù)組特性,簡單高效, 但是缺點在于內(nèi)存碎片。slab 提供了小對象的內(nèi)存分配方法, 實際上是一個多級緩存列表, 最小的分配單位稱為一個slab(一個或者多個連續(xù)頁), 被分配為多個對象來使用.
kswapd 是一個 daemon 進程, 對系統(tǒng)內(nèi)存做定時檢查, 一般是1秒一次. 如果發(fā)現(xiàn)沒有足夠的空閑頁面, 就做頁回收(page reclaiming), 將不再使用的頁面換出. 如果要換出的頁面臟了, 往往還需要寫回到磁盤或者swap.
bdflush 也是 daemon 進程, 周期性的檢查臟緩沖(磁盤cache), 并寫回磁盤. 不過在 Linux 2.6 之后, pdflush 取代了 bdflush, 前者的優(yōu)勢在于: 可以開多個線程, 而 bdflush 只能是單線程, 這就保證了不會在回寫繁忙時阻塞; 另外, bdflush 的操作對象是緩沖, 而 pdflush 是基于頁面的, 顯然 pdflush 的效率會更高.
觀察內(nèi)存
"pmap –x pid" 這個命令, 能將/proc/pid/maps中的數(shù)據(jù), 以更人性化的方式展示出來:

從上圖可以看到, 每一項內(nèi)容都清晰的標出了對象, 內(nèi)存起始地址(邏輯地址), 占用的內(nèi)存大小, 實際分配的內(nèi)存(RSS, 也就是常駐內(nèi)存), 以及臟內(nèi)存, 這些單位都是kb, 并給出了最終的統(tǒng)計結(jié)果. 統(tǒng)計結(jié)果的前兩項就是 top 中顯示的 VIRT 和 RSS.
VM tuning
這里就只關(guān)注 Linux 2.6 之后的情況了(2.4之前諸如 bdflush 就不在討論范圍之內(nèi)). 所有的VM可以調(diào)整的參數(shù)項, 都在/proc/sys/vm目錄下:

可以"sysctl vm.param"觀察參數(shù)的值, "sysctl –w vm.param=value"來修改參數(shù).具體的每一項參數(shù)的含義可以參考: https://www./doc/Documentation/sysctl/vm.txt.
-
pdflush調(diào)優(yōu), 其實這一塊跟磁盤IO關(guān)系比較緊.
-
dirty_writeback_centisecs, 默認是500, 單位是毫秒. 意思是每5秒喚醒 pdflush (多個線程), 將臟頁面寫回磁盤. 把這個參數(shù)調(diào)低可以增加 pdflush 被喚醒的頻率, 不過在內(nèi)核實現(xiàn)中, pdflush 在需要的時候會自動被喚醒, 所以這個參數(shù)的效果不可預(yù)期.
-
dirty_expire_centiseconds, 默認是3000, 單位是毫秒, 是指臟頁面的過期時間, 超過了這個時間, 就會觸發(fā) pdflush 做回寫.
-
dirty_background_ratio, 默認是10, 是指總內(nèi)存中臟頁面的百分比. 低于這個閾值時, pdflush 才會停止做回寫, 有的內(nèi)核版本的默認值是5.
-
dirty_ratio, 這也是一個百分比, 默認40, 是總內(nèi)存中臟頁面的百分比. 超過這個閾值, 就一定等待 pdflush 向磁盤回寫. 與 dirty_background_ratio 的區(qū)別在與: 如果 cache 的增長超過了 pdflush 的回寫速率時, 有可能 pdflush 來不及回寫, 在超過40\%這個閾值時, 進程就會等待, 直到 pdflush 處理到這個閾值之下. 此時就是一個IO瓶頸.
IO比較重的時候, 可以考慮的調(diào)優(yōu)手段: 首先嘗試調(diào)低 dirty_background_ratio, 其次是 dirty_background_ratio, 然后是 dirty_expire_centiseconds, dirty_writeback_centisecs 這一項可以不用考慮.
-
swapness, 這個表示了 swap 分區(qū)的使用程度, 等于0時表示盡可能不用 swap, 等于100表示積極的使用 swap, 默認是60. 這個參數(shù)取決于具體的需求.
-
drop_caches, 這個跟cache有關(guān), 默認是0. 設(shè)置不同的參數(shù)可以回收系統(tǒng)的 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

|