深入理解 Linux 內核內存管理機制與實現
Linux 內核的內存管理機制是通過使用虛擬內存來管理系統中的物理內存。虛擬內存將進程的地址空間劃分爲多個頁面,每個頁面大小通常爲 4KB 或更大。這些頁面被映射到物理內存或者交換空間上。
Linux 使用了分頁機制來實現虛擬內存管理。每個進程都有自己獨立的頁表,用於將虛擬地址轉換成物理地址。當進程訪問一個尚未映射到物理內存的虛擬地址時,發生缺頁異常,操作系統會根據需要從磁盤上加載相應的數據,並進行頁面映射。
Linux 還使用了各種高級技術來優化內存管理性能,例如頁表項緩存(Translation Lookaside Buffer, TLB)用於加速地址轉換過程,Copy-on-Write(寫時複製)機制減少對於共享頁面的不必要複製等。
一、CPU 尋址原理和分頁管理
1.1 尋址內存
CPU 訪問外設,有兩種類型,一個是內存空間,一個是 IO 空間。IO 空間,X86 通過 in/out 指令訪問外設,IO 空間只存在 X86 架構,在 RISC 架構不存在;
內存空間,CPU 通過指針訪問所有內存空間,內存空間分爲兩類,普通內存和位於內存空間的寄存器。其他設備的寄存器,比如通過 I2C 總線訪問觸摸屏的寄存器,與 CPU 內存空間無關。
MMU 原理:
對於一個支持 MMU 的 CPU,只要開啓 MMU,CPU 跟程序員視角一致,看到的永遠是虛擬地址;在訪問內存空間時,CPU 發一個虛擬地址 (指針),MMU 把虛擬地址映射爲物理地址。其他硬件設備,比如 DMA/IVE(硬件模塊不帶 mmu) 訪問也是物理地址。
虛擬地址和物理地址:
比如在兩個不同的進程 QQ/WECHAT 裏,可以分別定義一個變量,虛擬地址相同 (都爲 1G);真正訪問的時候,經過 MMU 映射到實際不同的物理地址。
每個進程都維護自己的頁表,相同的虛擬地址可以映射到不同的物理地址;
當 CPU 訪問 0x1234560 時,先查詢頁地址 0x1234 對應的物理地址 (比如 1M),560 爲頁內偏移
patch:
int *p=1m; //對指針賦值物理地址,是錯誤的
物理地址的本質
-
在 Linux 內核,物理地址定義爲一個無符號 64/32 位整數;
-
32 處理器虛擬地址最大 4G,物理地址不一定 32 位,可以大於 4G;
-
物理地址位數可以大於虛擬地址位數;
內存地址分爲三種不同的地址:邏輯地址、線性地址、物理地址。
-
邏輯地址:就是我們普遍應用使用的地址。每個邏輯地址都有一個段(段標識符)和偏移量(指定內存相對地址)表示,偏移量表示段開始的地方到實際地址之間的距離。段標識符也叫段選擇符,是一個 16 位長的字段。偏移量是一個 32 位長的字段。
-
線性地址:就是虛擬地址。
-
物理地址:內存真正的地址,用於內存芯片級內訓單元尋址。
內存控制單元 通過分段單元的硬件電路把邏輯地址轉換爲線性地址, 然後通過分頁單元的硬件電路將線性地址轉換爲物理地址。
在多處理器的系統中,多個 CPU 共享一個內存,但是在 RAM 芯片上的讀寫操作必須串行的進行,因此有一種 內存仲裁器(memory arbiter) 的硬件電路插在總線和每個 RAM 芯片之間,來控制 CPU 使用內存。這是個硬件電路,編程方面不必考慮。爲方便快速查找到段選擇符 ,從而找到地址,處理器提供 “段寄存器” 來存放段選擇符。段寄存器只存放段選擇符段寄存器只有 6 個,但是程序可以使用一個段寄存器用於不同的目的,方法是現將其保存在內存中,使用時再恢復。
段寄存器分爲 SS 、CS 、DS 、FS 、 ES 、GS 其中三個是特殊使用,另外三個作爲普通使用,可以指向任意的數據段
特殊使用:SS(棧段寄存器)指向包含當前程序棧的段,CS(代碼段寄存器)指向包含當前程序指令的段。特殊功能:包含一個兩位的字段,指明 cpu 的當前特權級,分爲 0(00)和 3,0 爲內核態,3(11)爲用戶態 DS(數據段寄存器)指向包含靜態數據或者全局數據的段,段描述符:(8 字節)不是段選擇符。段描述符描述了段的特徵,就像進程描述符與描述了進程的所有特徵一樣。放在全局描述符表(GDT)或者局部描述符表(LDT)。通常只有一個 GDT,每個進程除了有自己的 GDT 外一般都有附加的 LDT。描述符包含段的字節線性地址,描述符特權級等等,內容不做贅述。
80x86 提供一種非編程的寄存器(一個程序員不能設計的寄存器),提供給六個可編程的段寄存器使用,非編程的寄存器存放 8 字節的段描述符,由相應的段寄存器中的段選擇符來指定。每當一個段選擇符被裝入段寄存器時,相應的段描述符就被裝入非編程的 CPU 寄存器中。此時針對那個段的邏輯地址轉換就可以不訪問主存中的 GDT 或者 LDT,CPU 只需直接引用存放段描述符寄存器即可獲得對應的線性地址。當寄存器內的段改變時纔會再去訪問 GDT 或者 LDT。大致流程是這樣的,段選擇符被載入段寄存器,根據 GDT 或者 LDT 找到相應的段描述符放入非編程的寄存器中,訪問非編程的寄存器中的段描述符,獲得邏輯地址對應的線性地址。
linux 廣泛採用的不用類型的段和對應的描述符:
-
代碼段描述符:表示是一個代碼段,可以放在 GDT 和 LDT 中。
-
數據段描述符:表示是一個數據段,可以放在 GDT 和 LDT 中。
-
任務狀態段描述符:代表一個任務狀態段,例如處理器寄存器內容,只能出現在 GDT 中。
-
局部描述符表描述符:表示一個包含 LDT 的段,只能出現在 GDT 中。
1.2 硬件中的分段
Intel 微處理器以兩種不同的方式執行地址轉換,這兩種方式分別稱爲實模式和保護模式。實模式存在的主要原因是要維持處理器與早期模型兼容,並讓操作系統自舉。
段選擇符和段寄存器:一個邏輯地址由兩部分組成:一個段標識符和一個指定段內相對地址的偏移量。爲了快速方便地找到段選擇符,處理器提供段寄存器,段寄存器的唯一目的是存放段選擇符。這些段寄存器稱爲 cs,ss,ds,es,fs 和 gs。儘管只有 6 個段寄存器,但程序可以把同一個段寄存器用於不同的目的。6 個寄存器中 3 個有專門的用途:(1)cs 代碼段寄存器,指向包含程序指令的段;(2)ss 棧段寄存器,指向包含當前程序棧的段;(3)ds 數據段寄存器,指向包含靜態數據或者全局數據段;其它 3 個段寄存器作一般用途,可以指向任意的數據段。cs 寄存器還有一個很重要的功能:包含有一個兩位的字段,用以指明 CPU 的當前特權級 (0 最高, 3 最低)。
段描述符:每個段由一個 8 字節描述符表示,它描述了段的特徵。段描述符放在全局描述符 (GDT) 或局部描述符表 (LDT) 中。通常只定義一個 GDT,而每個進程除了存放在 GDT 中的段之外如果還需要創建附加的段,就可以有自己的 LDT。GDT 在主存中的地址和大小存放在 gdtr 控制寄存器,當前正被使用的 LDT 地址和大小放在 ldtr 控制寄存器中。有幾種不同類型的段以及和它們對應的段描述符,幾種 Linux 廣泛採用的類型:代碼段描述符,數據段描述符,任務狀態段描述符(TSSD), 局部描述表描述符(LDTD)。
快速訪問段描述符:爲了加速邏輯地址到線性地址的轉換,80x86 處理器提供一種附加的非編程的寄存器供 6 個可編程的段寄存器使用。
分段單元:(1) 先檢查段選擇符的 TI 字段,以決定段描述符保存在哪一個描述符中;(2) 從段選擇符的 index 字段計算段描述符的地址,index 字段的值乘以 8,這個結果與 gdtr 或 ldtr 寄存器中的內容相加;(3) 把邏輯地址的偏移量與段描述符 Base 字段的值相加就得到線性地址。
分頁單元把線性地址轉換爲物理地址。其中一個關鍵任務是把所請求的訪問類型與線性地址的訪問權限相比較,如果這次訪問是無效的,就產生一個缺頁異常。爲了效率起見,線性地址被分成以固定長度的組,稱爲頁 (page)。頁內部連續的線性地址被映射到連續的物理地址中。
這樣可以指定一個頁的物理地址和其存取權限,而不用指定頁所包含的全部線性地址的存取權限。分頁單元把所有的 RAM 分成固定長度的頁框 (物理頁)。把線性地址映射在物理內存地址的數據結構稱爲頁表。頁表存放在主存中,並在啓動分頁單元之前必須由內核對頁表進行適當的初始化。
常規分頁:32 位線性地址被分爲個域 (目錄、頁表、偏移量),線性地址的轉換分兩步完成,每一步都基於一種轉換表,第一種轉換表稱爲頁目錄表,第二種轉換表稱爲頁表。使用這種二級模式的目的在於減少每個進程頁表所需 RAM 的數量。二級模式通過只爲進程實際使用的那些虛擬內存區請求頁表來減少內存使用量。頁目錄項和頁表項有同樣的結構,每項都包含下面的字段 (present 標誌、Field 包含頁框物理地址最高 20 位的字段、Accessed 標誌、Dirty 標誌、Read/Write 標誌、User/Supervisor 標誌、PCD 和 PWT 標誌、Page Size 標誌、Global 標誌)。
擴展分頁:擴展分頁用於把大段連續的線性地址轉換成相應的物理地址,在這些情況下,內核可以不用中間頁進行地址轉換,從而節省內存並保留 TLB 項 (轉換後援緩衝器)。通過設置頁目錄項的 Page size 標誌啓用擴展分頁功能。在這種情況下,分頁單元把 32 位線性地址分成兩個字段 (Directory 最高 10 位、Offset 其餘 22 位)。擴展分頁和正常分頁的頁目錄項基本相同,除了:(1)Page SIze 標誌位必須被設置;(2)20 位物理地址字段只有最高 10 位是有意義的,這是因爲每個物理地址都是在以 4M 爲邊界的地方開始的,故這個地址的最低 22 位爲 0。
硬件保護方案:分頁單元和分段單元的保護方案不同。與段的 3 種存取權限 (讀、寫、執行) 不同的是,頁的存取權限只有兩種(讀、寫)。如果頁目錄項或頁表項的 Read/Write 標誌等於 0,說明相應的頁表或頁是隻讀的,否則是可讀寫的。
物理地址擴展 (PAE) 分頁機制:處理器所支持的 RAM 容量受連接到地址總線上的地址管腳數限制。通過將管腳數增加至 36 已經滿足這些需求 2^36=64GB, 不過需要把 32 位線性地址轉換爲 36 位物理地址才能使用所增加的物理地址。
Intel 爲了支持 PAE 已經改變了分頁機制:
-
(1)64GB 的 RAM 被分爲 2^24 個頁框,頁表項的物理地址字段從 20 位擴展到了 24 位;
-
(2) 引入一個叫作頁目錄指針表的頁表新級別,它由 4 個 64 位表項組成;
-
(3)cr3 控制寄存器包含一個 27 位的頁目錄指針表 (PDPT) 基地址字段;
-
(4) 當把線性地址映射到 4KB 的頁時 (頁目錄項中的 PS 標誌清 0),32 位線性地址按下列方式解釋:cr3 指向一個 PDPT、位 31-30 指向 PDPT 中 4 個項的一個、位 29-21 指向頁目錄中的 512 個項中的一個、位 20-12 指向頁表中 512 項中的一個、位 11-0 4KB 頁中的偏移量;
-
(5) 當把線性地址映射到 2MB 的頁時 (頁目錄項中的 PS 標誌置爲 1),32 位線性地址按下列方式解釋:cr3 指向一個 PDPT、位 31-30 指向 PDPT 中 4 個項的一個、位 29-21 指向頁目錄中的 512 個項中的一個、位 20-0 2MB 頁中的偏移量。
64 位系統中的分頁:所有 64 位處理器的硬件分頁系統都使用了額外的分頁級別。使用的級別數量取決於處理器的類型。
硬件高速緩存:爲了縮小 CPU 和 RAM 之間的速度不匹配,引入了硬件高速緩存內存。硬件高速緩存基於著名的局部性原理,該原理既適用程序結構也適用於數據結構。80x86 體系結構中引入了一個叫行的新單元,行由幾十個連續的字節組成,它們以脈衝突發模式在慢速 DRAM 和快速的用來實現高速緩存的片上靜態 RAM(SRAM) 之間傳送,用來實現高速緩存。高速緩存再被細分爲行的子集。
在一種極端的情況下,高速緩存可以是直接映射的,這時主存中的一個行總是存放在高速緩存中完全相同的位置。在另一種極端情況下,高速緩存是充分關聯的,這意味着主存中的任意一個行可以存放在高速緩存中的任意位置。但大多數高速緩存再某種程度上是 N - 路組關聯的,意味着主存中的任意一個行可以存放在高速緩存 N 行中的任意一行中。
高速緩存單元插在分頁單元和主內存之間,它包含一個硬件高速緩存內存和一個高速緩存控制器。高速緩存內存存放內存中真正的行。高速緩存控制器存放一個表項數組,每個表項對應高速緩存內存中的一個行。這種內存物理地址通常分爲 3 組:最高几位對應標籤,中間幾位對應高速緩存控制器的子集索引,最低幾位對應行內的偏移量。多處理器系統的每個處理器都有一個單獨的硬件高速緩存,因此它們需要額外的硬件電路用於保持高速緩存內容同步。
轉換後援緩存器 (TLB):80x86 處理器還包含了另一個稱爲轉換後援緩存器或 TLB 的高速緩存用於加快線性地址的轉換。當一個線性地址被第一次使用時,通過慢速訪問 RAM 中的頁表計算出相應的物理地址,同時物理地址被存放在一個 TLB 表項中,以便以後對同一線性地址的引用可以快速地得到轉換。
1.3Linux 中的分段
Linux 採用了一種同時適用於 32 位和 64 位系統的普通分頁模型。從 2.6.11 版本開始採用四級分頁模型:頁全局目錄、頁上級目錄、頁中間目錄、頁表。Linux 的進程處理很大程度上依賴於分頁。事實上,線性地址到物理地址的自動轉換使下面的設計目標變得可行:(1)給每個進程分配不同一塊不同的物理地址空間,這確保了可以有效地防止尋址錯誤;(2)區別頁 (即一組數據) 和頁框 (即主存中的物理地址) 之不同,這就允許存放在某個頁框中的一個頁,然後保存到磁盤上,以後重新裝入這同一頁時又可以被裝在不同的頁框中,這就是虛擬內存機制的基本要素。
分段可以給每一個進程分配不同的線性地址空間,而分頁可以把同一線性地址空間映射到不同的物理空間。與分段相比,Linux 更喜歡使用分頁方式,因爲:(1) 當所有進程使用相同的段寄存器時,內存管理變得更簡單,也就是說它們能共享同樣的一組線性地址;(2)Linux 設計目標之一是可以把它移植到絕大多數流行的處理器平臺上,然而,RISC 體系結構對分段的支持很有限。運行在用戶態的所有 Linux 進程都使用一對相同的段來對指令和數據尋址,這兩個段就是所謂的用戶代碼段和用戶數據段。
線性地址段:PAGE_SHIFT、PMD_SHIFT、PUD_SHIFT、PGDIR_SHIFT 等。
頁表處理:pte_t,pmd_t,pud_t 和 pgd_t 分別描述頁表項,頁中間目錄項、頁上級目錄和頁全局目錄項的格式。
物理內存佈局:內核將下列頁框記爲保留 (在不可用的物理地址範圍內的頁框、含有內核代碼和已初始化的數據結構的頁框),保留頁框中的頁絕不能被動態分配或交換到磁盤上。一般來說,Linux 內核安裝在 RAM 中從地址 0x00100000 開始的地方,也就是說從第二個 MB 開始,爲什麼內核沒有安裝在 RAM 第一個 MB 開始的地方?因爲 PC 體系結構有幾個獨特的地方必須考慮。
進程頁表:進程的線性地址空間分成兩部分:(1) 從 0x00000000 到 0xbfffffff 的線性地址,無論進程運行到用戶態還是內核態都可以尋址;(2) 從 0xc0000000 到 0xffffffff 的線性地址,只有內核態的進程才能尋址。但是在某些情況下,內核爲了檢索或存放數據必須訪問用戶態線性地址空間。
內核頁表:內核維持着一組自己使用的頁表,駐留在所謂的主頁內核全局目錄中。第一階段內核創建一個有限的地址空間,包括內核的代碼段和數據段、初始化頁表和用於存放動態數據結構的共 128KB 大小的空間。第二階段內核充分利用剩餘的 RAM 並適當地建立分頁表。具體實施:臨時內核頁表、當 RAM 小於 896MB 時的最終內核頁表、當 RAM 大小在 896MB 和 4096MB 之間時的最終內核頁表、當 RAM 大於 4096MB 時的最終內核頁表。
固定映射的線性地址:基本上是一種類似於 0xffffc000 這樣的常量線性地址。概念上類似於對 RAM 前 896MB 映射的線性地址。
處理硬件高速緩存和 TLB:內存尋址的最後一個主題是關於內核如何使用硬件高速緩存來達到最佳效果。硬件高速緩存和轉換後援緩存器 (TLB) 在提高現代計算機體系結構的性能上扮演着重要角色。爲了使高速緩存的命中率達到最優化,內核在下列決策中考慮體系結構:(1)一個數據結構中最常使用的字段放在該數據結構內的低偏移部分,以便它們能夠處於高速緩存的同一行中;(2)當爲一大組數據結構分配空間時,內核試圖把它們都存放在內存中,以便所有高速緩存行按同一方式使用。一般來說,任何進程切換都會暗示着更換活動頁表集,相對於過期頁表,本地 TLB 表項必須被刷新,不過在下列情況下將避免 TLB 被刷新:(1)當兩個使用相同頁表的普通進程之間執行進程切換時;(2)當在一個普通進程和一個內核線程間執行進程切換時。
Linux GDT:每一個 GDT 中包含的 18 個段描述符指向下列的段:
-
(1) 用戶態和內核態下的代碼段和數據段共 4 個;
-
(2) 任務狀態段 (TSS),每個處理器有 1 個,每個 TSS 相應的線性地址空間都是內核數據段相對應線性地址空間的一個小子集;
-
(3)1 個包括缺省局部描述表的段,這個段通常是被所有的進程共享的段;
-
(4)3 個局部線程存儲 (TLS) 段:這種機制允許多線程應用程序使用最多 3 個局部於線程的數據段,系統調用 set_thread_area()和 get_thread_area()分別爲正在執行的進程創建和撤銷一個 TLS 段;
-
(5)與高級電源管理 (AMP) 相關的字段:由於 BIOS 代碼段,所以當 Linux APM 驅動程序調用 BIOS 函數來獲取或者設置 APM 設備的狀態時,就可以使用自定義的代碼段和數據段;
-
(6)與支持即插用 (PnP) 功能的 BIOS 服務程序相關的 5 個段;
-
(7)被內核用來處理 “雙重錯誤” 異常的特殊的 TSS 段。
Linux LDT:缺省的局部描述符表存放在 default_ldt 數組中。在某些情況下,進程仍然需要創建自己的局部描述符表,modify_ldt() 系統調用允許進程修改自己的局部描述符表。
二、MMU 工作原理
MMU 是內存管理單元(Memory Management Unit)的縮寫。它是計算機系統中的一個硬件組件,負責將虛擬地址映射到物理地址,並提供對內存的訪問控制和保護機制。MMU 通常用於支持虛擬內存系統,可以將進程的虛擬地址空間劃分成多個頁,並將這些頁映射到物理內存上。通過使用 MMU,操作系統可以實現進程之間的隔離、動態內存分配和保護,提高了系統的安全性和可用性。
2.1MMU 的產生
許多年以前,當人們還在使用 DOS 或者更古老的操作系統的時候,計算機的內存還非常小,一般都是以 K 爲單位進行計算的,相應的,當時的程序規模也不大,所以內存容量雖然小,但還是可以容納當時的程序。
但隨着圖形界在的興起,用戶需求的不斷增大,應用程序的規模也隨之膨脹起來,終於一個難題出現在程序員的面前,那就是應用程序太大,以至於內存容納不下該程序。
通常解決的辦法是把程序分割成許多份稱爲覆蓋塊(overlay)的片段。
覆蓋塊 0 首先運行,結束時他將調用另一個覆蓋塊。
雖然覆蓋塊的交換是由 OS 完成的,但是必須先由程序員先進行分割,這是一個費時費力的工作,而且相當枯燥。
人們必須找於更好的辦法從根本上解決這個問題。
不久人們找到了一個辦法,這就是虛擬存儲器(virtual memory)。
1)虛擬存儲器(Virtual Memory)
虛擬存儲存的基本思想是:程序、數據、堆棧的總的大小可以超過物理存儲器的大小,操作系統把當前使用的部分保留在內存中,而把其他未被使用的部分保存在磁盤上。
比如,對一個 16MB 的程序 和一個內存只有 4MB 的機器,OS 通過選擇,可以決定各個時刻將哪 4MB 的內容保留在內存中,並需要時在內存和磁盤間交換程序片段,這樣就可以把這個 16MB 的程序運行在一個具有 4MB 內存機器上了。而這個 16M 的程序在運行前不必由程序員進行分割。
任何時候,計算機上都存在一個程序能夠產生的地址集合,我們稱之爲地址範圍。
這個範圍的大小由 CPU 的位數決定,例如一個 32 位的 CPU ,它的地址範圍是0x0 ~ 0xFFFF FFFF (4G),而對於一個 64 位的 CPU ,它的地址範圍爲0x0 ~ 0xFFFF FFFF FFFF FFFF (64T)。
這個範圍就 是我們程序能夠產生的地址範圍,我們把這個地址範圍稱爲虛擬地址空間,該空間中的某一個地址我們稱之爲虛擬地址。
與虛擬地址空間 和 虛擬地址相對應的則是物理地址空間 和 物理地址,大多數時候,我們的系統所具備的物理地址空間只是虛擬地址空間的一個子集,這時舉一個最簡單的例子直觀的說明這兩者,對於一臺內存爲 256MB 的 32Bit x86 主機來說,它的虛擬地址空間範圍是0x0 ~ 0xFFFF FFFF (4G),而物理地址空間範圍是0x0000 0000 ~ 0x0FFF FFFF ( 256MB )。
在沒有使用虛擬地址的機器上,虛擬地址被直接送到內存總線上,使具有相同地址的物理存儲被讀寫。而使用了虛擬存儲的情況下,虛擬地址不是被直接送到內存地址總線上,而是送到內存管理單元— MMU。
MMU 由一個或一組芯片組成,一般存在於協處理器中,其功能是把虛擬地址映射爲物理地址。
-
CPU 看到的是 Virtual Adress (程序中的邏輯地址)
-
Caches 和 MMU 使用的是 MVA (實際的虛擬地址 MVA = (pid << 25) | VA)
-
實際物理設備使用的是 Physical Address (物理地址)
2)MMU 的工作過程
大多數使用虛擬存儲器的系統都使用一種稱爲分頁(paging)。
虛擬地址空間劃分爲頁(page)的單位,而相應的物理地址空間也被進行劃分,單位是頁框(frame)。
頁和頁框的大小必須相同。
接下來配全圖片,以一個例子說明頁與頁框之間在 MMU 的調度下是如何進行映射的:
在這個例子中,我們有一個可以生成 16 位地址的機器,它的虛擬地址範圍從0x0000 ~ 0xFFFF(64k),而這臺機器只有 32K 的物理地址,因此它可以運行 64K 的程序,但該程序不能一次性調入內存運行。
這臺機器必須有一個達到可以存放 64K 程序 的外部存儲器(例如磁盤或 Flash) 以保證程序片段在需要時可以被調用。
這個例子中,頁的大小爲 64K ,頁框大小與頁相同(這點必須保證的,內存和外圍存儲器之間傳輸總是以頁爲單位),對 應 64K 的虛擬地址和 32K 的物理存儲器,它們分別包含了 16 個頁 和 8 個頁框。
執行下面這些指令:
MOVE REG,0// 將 0 號地址的值傳遞進寄存器 REG
虛擬地址 0 將被送往 MMU,MMU 看到該虛擬地址落在頁 0 範圍內(頁 0 範圍是 0 到 4095), 從上圖我們可以看出頁 0 所對應的(映射)的頁框爲 2(頁框 2 的地址範圍是 8192 到 12287)。
因此,MMU 將該虛擬地址轉化爲物理地址 8192, 並把地址 8192 送到地址總結上。
內存對 MMU 的映射一無所知,它只看到一個對地址 8192 的讀請求並執行它,MMU 從而將 8192 到 12287 換虛擬地址解析爲對應的物理地址 0 到 4096 。
MOVE REG , 20500
被轉換爲----> MOVE REG, 12308
因爲虛擬地址 20500 在虛頁 5(虛擬地址範圍是 20480 到 24575)距開頭 20 個字節處,虛頁 5 映射到頁框 3(頁框 3 的地址範圍是 12288 到 16383),於是被映射到物理地址12288 + 20 = 12308。
MOV REG , 32780
虛擬地址 32780 落在頁 8 的範圍內,從上圖我們看出,頁 8 並沒有被 有效的進行映射(該頁被打上 X),這時又會發生什麼呢?
MMU 注意到這個頁沒有被映射,於是通知 CPU 發生一個缺頁故障(page fault),這種情況下,操作系統必須處理這個頁故障,它必須從 8 個物理頁框中找到一個很少被使用的頁框,並把該頁框的內容寫入外圍存儲器(這個動作被稱爲 page copy),隨後把需要引用的頁(本例 是頁 8)映射到剛纔被釋放的頁框中(這個動作被稱爲修改映射關係),然後重新執行產生故障的指令(MOV REG, 32780).
假定操作系統,決定釋放頁框 1, 以使以後任何對虛擬地址 4K 到 8K 的訪問都引起故障而使操作系統做出適當的動作。
其次它把虛頁 8 對應的頁框號由 X 變爲 1, 因此得新執行 MOV REG, 32780,MMU 將 32780 映射爲 4180。
我們已經知道,大多數使用虛擬存儲器的系統都使用一種稱爲分頁(paging)的技術,就象我們剛纔所舉的例子,虛擬地址空間被分爲大小相同的一組頁,每個頁有一個用來標示它的頁號(這個頁號一般是它在該組中的索引,這點和 C/C++ 中的數組相似)。
在上面的例子中 0~4K 的頁號爲 0,4~8K 的頁號爲 1,8~12K 的頁號爲 2,以此類推。
而虛擬地址(注意:是一個確定的地址,不是一個空間)被 MMU 分爲 2 個部分,第一部分是頁號索引(page Index),第二部分則是相對該頁首地址的偏移量(offset).
我們還是以剛纔那個 16 位機器結合下圖進行一個實例說明,該實例中,虛擬地址 8196 被送進 MMU,MMU 把它映射成物理地址。16 位的 CPU 總共能產生的地址範圍是 0~64K, 按每頁 4K 的大小計算,該空間必須被分成 16 個頁。而我們的虛擬地址第一部分所能夠表達的範圍也必須等於 16(這樣才能索引到該頁組中的每一個頁), 也就是說這個部分至少需要 4 個 bit。
該地址的頁號索引爲0010(二進制碼),即索引的頁爲頁 2,第二部分爲000000000100(二進制),偏移量爲 4。
頁 2 中的頁框號爲 6(頁 2 映射在頁框 6,見上圖),我們看到頁框 6 的物理地址是 24~28K。於是 MMU 計算出虛擬地址 8196 應該被映射成物理地址 24580(頁框首地址 + 偏移量 = 24576+4=24580)。
同樣的,若我們對虛擬地址 1026 進行讀取,1026 的二進制碼爲0000010000000010,page index="0000"=0,offset=010000000010=1026。
頁號爲 0,該頁映射的頁框號爲 2,頁框 2 的物理地址範圍是 8192~12287,故 MMU 將虛擬地址 1026 映射爲物理地址 9218(頁框首地址 + 偏移量 = 8192+1026=9218)。
以上就是 MMU 的工作過程。
2.2 虛擬內存管理
現代操作系統普遍採用虛擬內存管理(Virtual Memory Management)機制,這需要處理器中的 MMU(Memory Mangement Unit,內存管理單元)提供支持。
首先引入兩個概念,虛擬地址和物理地址:
-
如果處理器沒有 MMU,或者有 MMU 但沒有啓用,CPU 執行單元發出的內存地址將直接傳到芯片引腳上,被物理內存芯片接收,這稱爲物理地址。
-
如果處理器啓用了 MMU,CPU 執行單元發出的內存地址將被 MMU 截獲,從 CPU 到 MMU 的地址稱爲虛擬地址,而 MMU 將這個地址翻譯成另一個地址,發到 CPU 芯片的外部地址引腳上,也就是將 VA 映射成了 PA 了。
如果是 32 位處理器, 則內存地址總線是 32 位的,與 CPU 執行單元相連,而經過 MMU 轉換後的外地址總線則不一定是 32 位。
也就是說,虛擬地址空間與物理地址空間是獨立的,32 位處理器的虛擬地址空間是 4GB,而物理地址空間既可以大於也可以小於 4G。
MMU 將 VA 映射到 PA 是以頁(page)爲單位的,32 位處理器的頁尺寸通常是 4KB。
例如:
MMU 可以通過一個映射項將 VA 的一頁0xB7001000 - 0xB7001FFFF映射到 PA 的一頁0x2000 ~ 0x2FFF。
如果 CPU 執行單元要訪問虛擬地址 0xB7001008,則實際訪問到的物理地址是 0x2008。
物理內存中的頁稱爲物理頁幀(page frame), 虛擬內存的哪個頁面映射到物理內存的哪個頁幀是通過頁表(Page Table)來描述的,頁表保存在物理內存中,MMU 會查找頁表來確定一個 VA 應該映射到什麼 PA.
操作系統和 MMU 是這樣配合的:操作系統在初始化或分配、釋放內存時會執行一些指令在物理內存中填寫頁表,然後用指令設置 MMU,告訴 MMU 頁表在物理內存中的什麼位置。
設置好之後,CPU 每次執行訪問內存的指令都會自動引發 MMU 做查表和地址轉換操作,地址轉換操作由硬件自動完成,不需要用指令控制 MMU 去做。
我們在程序中使用的變量和函數都有各自的地址,程序被編譯後,這些地址就成了指令中的地址,指令中的地址被 CPU 解釋執行,就成了 CPU 的執行單元發出的內存地址,所以在啓用 MMU 的情況下,程序中使用的地址都是虛擬地址,都會引發 MMU 做查表和地址轉主換操作。
那爲什麼要設計這麼複雜的內存管理機制呢? 多了一層 VA 到 PA 的轉換到底換來什麼好處?
MMU 除了做地址轉換之外,還提供內存保護機制,各種體系結構都有用戶模式(User Mode)和特權模式(Privileged Mode)之分,操作系統可以在頁表中設置每個內存頁面的訪問權限,有些頁面不允許訪問,有些頁面只有在 CPU 處於特權模式時才允許訪問,有些頁面在用戶模工和特權模式都可以訪問,訪問權限又分爲可讀、可寫 和可執行三種。
這樣設定好之後,當 CPU 要訪問一個 VA 時,MMU 都會檢查 CPU 當前處於用戶模式還是特權模式,訪問內存的目的是讀數據、寫數據、還是取指令,如果和操作系統設定的頁面權限相符,就允許訪問,把它轉換成 PA ;如果不允許訪問,就產生一個異常(Exception)。
異常處理過程和中斷類似,不同的是中斷由外部設備產生而異常由 CPU 內部產生,中斷產生的原因和 CPU 當前執行的指令無關,而異常的產生就是由於 CPU 當前執行的指令出了問題,例如,訪問內存的指令被 MMU 檢查出權限錯誤,除法指令的除數爲 0 都會產生異常。
2.3 用戶空間和內核空間
通常操作系統把虛擬地址劃分爲用戶空間和內核空間,例如 X86 平臺的 Linux 系統虛擬地址空間是0x00000000 - 0xFFFFFFFF,前 3GB(0x00000000 - 0xBFFFFFFF)是用戶空間,後 1GB(0xC0000000 - 0xFFFFFFFF)是內核空間。
用戶程序加載到用戶空間,在用戶模式下執行,不能訪問內核中的數據,也不能跳轉到內核代碼中執行。
這樣可以保護內核,如果一個進程訪問了非法地址,頂多這一個進程崩潰,而不會影響到內核和整個系統的穩定性。
CPU 在產生中斷和異常時不僅會跳轉到中斷或異常服務程序,還會自動切換模式,從用戶模式切換到特權模式,因此從中斷或異常服務程序可以跳轉到內核代碼中執行。
事實上,整 個內核就是由各種中斷和異常處理程序組成的,在正常情況下 ,處理器在用戶模式執行用戶程序,在中斷或異常情況下處理器切換到特權模式執行內核程序,處理完中斷或異常之後再返回用戶模式繼續執行用戶程序。
2.4MMU 以及 RWX 權限、kernel 和 user 模式權限
32 位處理器,頁表有 32 位,但實際維護頁表只用到高 20 位,低 12 位用來存放其他信息比如權限
CPU 訪問一個地址,不僅要做虛擬 / 物理地址頁表地址查詢,還要檢查頁表權限,出現非法操作時,MMU 攔截 CPU 訪問請求,並報 pagefault,CPU 收到 MMUpagefault 中斷時,報段錯誤,發信號 11,進程默認信號處理方式是掛掉。
找不到物理地址,或者權限不對,發生段錯誤;
tips:gdb layout src
meltdown 漏洞,利用時間旁路攻擊原理,從用戶空間獲取內核空間數據;
MMU 作用總結:
(1). 內存隔離:防止應用程序訪問不屬於自己的空間,隔離應用對其它應用空間、內核空間的訪問。因爲它們只能訪問自己的虛擬空間,每個進程的虛擬空間都是獨立的。
(2). 權限管理:不同地址段擁有不同的訪問權限,不能越權訪問。
(3). 地址映射:將不連續的物理地址映射爲連續的虛擬地址,並可以通過 swap 機制可以獲得比實際內存大得多的使用空間。
前兩者實現數據安全,第三條爲程序提供空間便利。
再補充兩點:
-
直接使用物理內存,當即將運行的程序內存不足時,需要選擇一個進程整體換出,這導致有大量數據的換出與換入,效率低下;即 MMU 可以解決內存使用效率問題;
-
有效解決碎片化問題;
三、 Linux 內存
3.1 內存的 zone: DMA、Normal 和 HIGHMEM
32 位 Linux 內核空間物理地址映射在 3G~4G,分三個區域,
ZONE_DMA(0~16M):DMA 內存分配區;
ZONE_NORMAL(16MB~896MB): 普通映射的內存區域;
ZONE_HIGHMEM(896MB~): 高端內存區域,其中的頁不能永久映射到內核地址空間;內核一般不使用,如果要使用,通過 kmap 做動態映射;
存在高端內存的原因是,當物理內存大於虛擬內存尋址範圍 (1G) 時,需要做非線性映射,才能在訪問所有物理內存空間範圍。32 系統一般都劃分 896M 以上物理地址爲高端內存。
在目前流行的 64 位 CPU 中,由於虛擬尋址位數足夠,就不存在高端內存概念了。
896M 以下,開機直接映射好 (通過 virt_to_phys/phys_to_virt 簡單線性映射),896MB 以上動態分配,可以被內核和用戶空間訪問;
對於 64 位 CPU 有足夠尋址能力,就不存在 869MB 問題了,64 位處理器,一般沒有 HGIHMEM ZONE;
對於一個嵌入式設備,內存太小,也不存在 HIGHMEM 區域;
DMAzone 存在的根本原因是 DMA 硬件有缺陷,比如一個典型 32 位系統 ISA 設備的 DMA 只有 24 位尋址能力,也就是隻能訪問內存的前 16M 內存,而整個系統的最大物理內存尋址甚至可以大於 1G,超過內核的虛擬空間;
DMA 沒有 MMU,所以只能訪問連續物理內存;少數最新的 DMA 具有 MMU,也可以訪問物理不連續的內存,MMU 頁表映射即可。
當爲一個 DMA 有缺陷設備申請內存時,傳入標記 GFP_DMA,在 DMA_ZONE 區域分配內存,如果 DMA 可以訪問所有內存空間,則不用受限於 DMA_ZONE。而其他普通設備,也可以申請 DMA_ZONE.
申請 DMA 內存,會填入設備訪問範圍,根據範圍確定申請的內存區域;
DMA 內存不一定來自於 DMA ZONE,DMA ZONE 也不一定用於 DMA,如果系統所有 DMA 都沒有缺陷,則不存在 dma_zone;
DMA 直接訪問內存,不會讓外設訪問速度更快,DMA 最大好處是解放 CPU 資源,速度受限外設總線及訪問頻率;
3.2Linux 內存管理 Buddy 算法
Buddy 算法直面物理內存,Buddy 算法把內存中的所有頁面按照 2 的冪次進行分塊管理,分配的時候如果沒有相應大小塊,就把大的塊二分成小塊。釋放的時候,回收的塊跟相鄰的空閒夥伴塊又能合併成大塊;
BUDDY 頁面的分配和使用情況可以通過 proc 接口 / proc/buddyinfo 來查看:
Buddy 有個缺陷,會造成許多內存碎片,比如總和還剩餘很大,但是沒有足夠連續的空餘內存可用。
早期採用預留分配方法,給予顯卡,攝像頭等預留內存,即使設備不使用,也會預留。
3.3 連續內存分配器 (CMA)
分配專門的 CMA 區域,用於 DMA 設備的內存申請,比如攝像頭,當攝像頭設備不用時,該區域可以用於其他內存分配;當使用攝像頭時,將該區域內的所有已申請內存,拷貝到其他分散的頁,並且修改對應頁表。
這樣 CMA 大塊連續內存,就不會被浪費掉,也保持了大塊連續內存的訪問需求;
把不同的 DMA 設備,放在不同的 CMA,就不會導致內存碎片化;
3.4 內存的動態申請
1)slab、kmalloc/kfree、/proc/slabinfo 和 slabtop
Buddy 是直面物理內存的,所有的內存分配,最終都通過 Buddy 的 get_free_page/page_alloc 分配;
Buddy 的粒度太大,最小分配一頁 (4k); 而我們常常需要分配小內存;
所以 Linux 引入一個二級分配的概念:
-
- 內核分配內存,調用 kmalloc()/kfree()–調用 slab–再調用 Buddy ;
-
- 用戶空間 malloc/free–調用 C 庫–C 庫通過 brk/mmap 調用 Buddy;
free 釋放的內存,只是釋放給 C 庫,未必真正釋放;釋放給 C 庫的內存,其他進程無法使用,共享代碼段,數據段獨立。
slab 原理:就是從 Buddy 拿到一個 / 多個頁,分成多個相同大小的塊,比如進程控制塊 task_struct 這種內核中常用的結構體,可以先從 Buddy 申請一個 slab 池,實際 kmalloc 分配的時候,直接從 slab 裏分配。可以提高速度,也可以使內存得到充分使用,減少內存碎片,每一個 slab 裏的所有塊,大小一定是相同的。
sudo cat /proc/slabinfo
cat /proc/meminfo
slab 也是一個內存泄漏的源頭,應用程序 free 內存,未必一定釋放,可以通過 mallopt 設置 free 內存的觸發閾值,觸發閾值,free 才真正在 Buddy 釋放。
(2) kmalloc / vmalloc 詳細過程
(1)kmalloc 從低端內存區域分配,該區域是開機就一次性映射好;所以 kmalloc 申請,不需要做映射, 且在物理內存上是連續的,phys_to_virt/virt_to_phys 只是一個簡單的內存線性偏移。
(2)vmalloc 申請的虛擬內存,可以從普通物理內存空間分配,也可以從低端內存分配;調用 vmalloc 申請,會有一個虛擬地址到物理地址映射過程。
ioremap 映射的也是 vmalloc 虛擬地址,不過不用申請內存,ioremap 映射的是寄存器;
映射跟分配是兩回事,低端被映射之後,不一定被使用;
sudo cat /proc/vmallocinfo |grep ioremap
//可以查看虛擬地址映射物理地址/寄存器關係
kmalloc 和 vmalloc 區別大概可總結爲:
-
(1)vmalloc 分配的一般爲普通內存,只有當內存不夠的時候才分配低端內存;kmalloc 從低端內存分配。
-
(2)vmalloc 分配的物理地址一般不連續,而 kmalloc 分配的物理地址連續,兩者分配的虛擬地址都是連續的;
-
(3)vmalloc 分配的一般爲大塊內存,而 kmallc 一般分配的爲小塊內存;
3.5 釋放的原理和細節
用戶空間 malloc/free 與內核之間的關係
問題 1:malloc:VSS , RSS
p = malloc(100M);//分配過程
-
在進程的堆中找到一個空閒地址,比如 1G,創建一個 VMA(virtual memoryarea), 權限可讀寫;
-
將 p=1G~1G+100M 全部映射到零頁 (標記爲只讀);
-
當讀 p 虛擬內存的時候,全部返回 0,實際上任何內存都未分配;
-
當寫內存時,比如寫 1G+30M 處,而該地址映射向零頁 (只讀),MMU 發現權限錯誤,報 page fault,CPU 可以從 page fault 中得到寫地址和權限,檢查 vma 中的權限,如果 vma 標明可寫,那麼觸發缺頁中斷,內核分配一個 4K 物理頁,重新填寫 1G+30M 頁表,使其映射到新分配的物理頁;
這樣該頁就分配了實際的物理內存,其他所有未使用到的虛擬地址亦然映射到零頁。
如果檢查 vma 發現沒有可寫權限,則報段錯誤;
如果檢查 vma 合法,但是系統內存已經不夠用,則報 out of memory(OOM)
在真正寫的時候,纔去分配,這是按需分配,或者惰性分配;
只申請了 VMA,未實際拿到物理內存,此時叫 VSS;拿到實際內存後是 RSS(駐留內存);
屬於按需分配 demanding page,或者惰性分配 lazy allocation;
對於代碼段 (實際讀時,才實際去分配內存,把代碼從硬盤讀到內存),數據段都是類似處理,實際使用時,纔會實際分配內存;
內存耗盡 (OOM)、oom_score 和 oom_adj
在實際分配內存,發現物理內存不夠用時,內核報 OOM,殺掉最該死進程 (根據 oom_score 打分),釋放內存;
打分機制,主要看消耗內存多少 (先殺大戶),mm/oom_kill.c 的 badness() 給每個進程一個 oom_score,一般取決於:駐留內存、pagetable 和 swap 的使用;
oom_score_adj:oom_score會加上oom_score_adj這個值;
oom_adj:-17~15的係數調整
關掉交換內存分區:
sudo swapoff -a
sudo sh -c ‘echo 1 \> /proc/sys/vm/overconmit_memory’
git grep overcommit_memory
dmesg //查看oom進程的oom_score
查看 chrome 進程的 oom_score
pidof chrome
cd /proc/28508/
cat oom_score
由上圖可知,
1 將 oom_adj 值設置越大,oom_score 越大,進程越容易被殺掉;
- 將 oom_adj 設置更大時,普通權限就可以;要將 oom_adj 設置更小,需要 root 權限;
即設置自身更容易死掉,自我犧牲是很容易的,設置自己不容易死,是索取,需要超級權限纔可以;
Android 進程生命週期與 OOM
android 進程的生命週期靠 OOM 來驅動;
android 手機多個進程間切換時,會動態設置相應 oom_adj,調低前臺進程的 oom_adj,調高後臺進程的 oom_adj,當內存不夠時,優先殺後臺進程。所以內存足夠大,更容易平滑切換。
對於簡單的嵌入式系統,可以設置當 oom 時,是否 panic
cd /proc/sys/vm
cat panic_on_oom
mm/oom_kill.c oom_badness() 函數不停調整進程 oom_score 值;
總結一個典型的 ARM32 位,Linux 系統,內存分佈簡圖;
ARM32 位內核空間 3G-16M~4G
3G-16M~3G-2M 用來映射 KO
3G-2M~3G: 可以用 kmap 申請高端內存,用 kmap,建立一個映射,臨時訪問頁,訪問完後 kunmap 掉;
進程的寫時拷貝技術,mm/memory.c/cow_user_page 用到 kmap 映射;
其他練習:
1.看/proc/slabinfo,運行slabtop
運行mallopt.c程序:mallopt等
運行一個很耗費內存的程序,觀察oom memory
通過oom_adj調整firefox的oom_score
四、進程的內存消耗和內存泄漏
4.1 進程的 VMA
(1) 進程地址空間
在 Linux 系統中,每個進程都有自己的虛擬內存空間 0~3G;
內核空間只有一個 3G~4G;
進程通過系統 API 調用,在內核空間申請內存,不統計在任何用戶進程;進程消耗內存,單指用戶空間內存消耗;
(2)VMA 列表
LINUX 用 task_struct 來描述進程,其中的 mm_struct 是描述內存的結構體,mm_struct 有一個 vma 列表,管理當前進程的所有 vma 段。
每個進程的內存由多個 vma 段組成:
(3) 查看 VMA 方法:
1.pmap
由圖知,從接近 0 地址開始,第一個 4K 是隻讀代碼段,第二個 4K 是隻讀數據段,還有其他共享庫代碼段,堆棧等;
可見一個進程的 VMA 涵蓋多個地址區域 **,但並沒有覆蓋所有地址空間 **。VMA 未覆蓋的地址空間是 illegal 的,訪問這些地址,缺頁中斷,發生 pagefault.
2.cat /proc/pid/maps
讀文件形式,與 pmap 一一對應;
3.cat /proc/pid/smaps
更詳細的描述
00400000-00401000 r-xp 00000000 08:15 20316320 /home/leon/work/linux/mm/a.out
Size: 4 kb
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 4 kB
Private_Dirty: 0 kB
Referenced: 4 kB
Anonymous: 0 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
VmFlags: rd ex mr mw me dw sd
查看 VMA 的三個方法對比
VMA 的來源,代碼段,數據段,堆棧段。
VMA 是 linux 最核心數據結構之一。
4.2page fault 的幾種可能性,major 和 minor
mmu 給 cpu 發 page fault 時,可以從寄存器讀到兩個元素,pagefault 地址,pagefault 原因。
(1) 訪問 Heap 堆 (首次申請,不是從 Libc 獲取),第一次寫,發生 pagefault,linux 檢查 VMA 權限,發現權限合法,發缺頁中斷,申請一頁內存,並更新頁表。
(2) 訪問空區域,訪問非法,發段錯誤;
(3) 訪問代碼段, 在此區域寫,報 pagefault,檢查權限發現錯誤,報段錯誤;
(4) 訪問代碼段, 在此區域讀 / 執行,linux 檢查權限合法,若代碼不在內存,那麼申請內存頁,把代碼從硬盤讀到內存
伴隨 I/O 的 pagefault, 叫 major pagefault, 否則 minor pagefault.
major pagefault 耗時遠大於 minor pagefault.
\time -v python hello.py
內存是如何被瓜分的: :vss、rss、pss 和 uss
rss 是不是代表進程的內存消耗呢,NO。
-
VSS:單個進程全部可訪問的地址空間,但未必所有虛擬地址都已經映射物理內存;
-
RSS:駐留內存,單個進程實際佔用的物理內存大小 (不十分準確的描述);上圖的進程 1
-
PSS: 比例化的內存消耗,相對 RSS,將共享內存空間按被共享進程數量比例化;上圖的 C 庫 4 被三個進程共享,所以 4/3;
-
USS:進程獨佔內存,比如上圖的堆 6。
案例,連續運行兩次 a.out,查看內存佔用情況
運行程序 a.out
./a.out &
pidof a.out
cat /proc/pid/smaps |more
./a.out &
pidof a.out
cat /proc/pid_2/smaps |more
PSS 變化
shared_clean
private_clean
4.3 應用內存泄漏的界定方法
-
統計系統的內存消耗,查看 PSS。
-
檢查有沒有內存泄漏,檢查 USS
./smem
smem –pie=command
smem –bar=command
android 裏面有類似的工具,procmem/procrank
smem 分析系統內存使用是通過 / proc/smaps 的,procrank 是通過分析 / proc/kpagemap。
4.4 應用內存泄漏的檢測方法:valgrind 和 addresssanitizer
其他查詢內存泄漏工具:
valgrind:
gcc -g leak-example.c -o leak-example
valgrind --tool=memcheck --leak-check=yes ./leak-example
20468 HEAP SUMMARY:
20468 in use at exit: 270,336 bytes in 22 blocks
20468 total heap usage: 44 allocs, 22 frees, 292,864 bytes allocated
20468
20468 258,048 bytes in 21 blocks are definitely lost in loss record 2 of 2
20468 at 0x4C2DB8F: malloc (in
/usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
20468 by 0x4005C7: main (leak-example.c:6)
valgrid 是在虛擬機跑 APP,速度很慢;
新版 gcc4.9 以後集成了 asan
asan:
gcc -g -fsanitize=address ./leak-example.c
gcc -fuse-ld=gold -fsanitize=address -g ./lsan.c
#1 0x40084e in main lsan.c:9
內存泄漏不一定在用戶空間,排查內核空間,檢測 slab , vmalloc
slaptop, 檢查申請和釋放不成對。
在內核編譯,打開 kmemleak 選項。
4.5 工程調試
內存泄漏問題一般步驟:
(1) meminfo, free 多點採樣確認有內存泄漏。
(2) 定位, smem 檢查用戶空間 USS 在不斷增加。
(3) slab, 檢查內核空間。
cat /proc/slabinfo
其他查看內存信息的方法
cat /proc/meminfo
cat /proc/buddyinfo
cat /proc/zoneinfo
cat /proc/meminfo
五、內存與 I/O 的交換
5.1page cache 頁緩存
Linux 讀寫文件過程;
read: 用戶進程調用 read 命令,內核查詢讀取的文件內容是否在內存 (內核 pagecache) 中,若該頁內容緩存在內存中,直接讀取返回給用戶進程;若緩存不存在,則啓動 BIO,從硬盤讀取該頁面到內存,再送給用戶進程;
write 過程:比如往某文件 5K 處寫入 10byte,內核先查詢該頁是否在內存緩存 (內核 pagecache),不在,同 read, 從硬盤讀取該頁 4~8K 到內存,再往 5k 處寫入 10byte, 標明該頁爲髒頁;
寫回磁盤時機,則由內存管理的 BIO 機制決定。
Linux 讀寫文件兩種方式:read/write,mmap
相同點:都經過 page cache 中介操作磁盤文件;
區別:
-
read/write:有一個用戶空間和內核空間的拷貝過程;
-
mmap:指針直接操作文件映射的虛擬地址,省略了用戶空間和內核空間的拷貝過程。
缺點是,很多設備不支持 mmap,比如串口,鍵盤等都不支持。
5.2free 命令的詳細解釋
total: 系統總的內存
第一行 used: 所有用掉的內存,包括 app,kernel,page cached(可回收);
第一行 free: =total-used
第一行 buffers/cached:page cached 內存
第二行 free = 第一行 free+buffers+cached
114380=31040+8988+74352
第二行 used = 第一行 used-buffers-cached
40072=123412-8988-74352
第一行 used 是從 buddy 的角度,統計所有用掉的內存 (包括 page cached);
第二行 uesed,是從進程角度,包括 app/kernel/ , 不含 page cached(可回收)。
cached/buffer 區別:
cached: 通過文件統訪問;
buffer: 裸地址直接訪問,存放文件系統的元數據 (組織數據的數據);
新版 free,去掉了 buffer/cached 的區別,加入一個 available,預估可用內存
在 cat /proc/meminfo 也可以看到預估項 MemAvailable
查看實現方法 git grep MemAvailable
vim fs/proc/meminfo.c
搜 MemAvailable,實現函數 si_mem_available
git grep si_mem_available
vim mmpage_alloc.c-- >long si_mem_available(void)
5.3read、write 和 mmap
mmap 映射文件過程,實際上在進程創建一個 vma,映射到文件,當真正讀 / 寫文件時,分配內存,同步磁盤文件內存。
其本質是虛擬地址映射到 page cached, page cached 再跟文件系統對應。
這裏的 page cached 是可以回收的。
5.4file-backed 的頁面和匿名頁
在一個 elf 文件中,代碼段的本質,就是對應 page cached;
真正執行到的代碼,纔會從磁盤讀入內存,並且還可能被踢走,下次再執行,可能需要重新從磁盤讀取。
Reclaim 可回收的有文件背景的頁面,叫 file-back page, 可回收
匿名頁:anon, 不可回收,常駐內存;
pmap pid
Anon: stack、heap、 CoW pages
File-backed:代碼段,映射文件,可回收。
5.5swap 以及 zRAM
案例,當同時運行 word 和 qq,word 需要 400M 匿名頁,qq 需要 300M 匿名頁,而物理內存只有 512M,如何運行呢,此時僞造一個可 swap 的文件,供 anon 匿名頁交換。
在內核配置 CONFIG_SWAP,支持匿名頁 swap,不配置,普通文件 swap 依然支持;
SWAP 分區,對應 windows 的虛擬內存文件 pagefile.system。
5.6 頁面回收和 LRU
局部性原理:最近活躍的就是將來活躍的,最近不活躍的,以後也不活躍。
包括時間局部性,空間局部性。
LRU:Least Recently Used 最近最少使用,是一種常用的頁面置換算法,
真實統計過程,無關進程,只統計頁面的活躍度;
案例,開啓瀏覽器 chrome;長期不使用,運行 oom.c 消耗掉系統所有內存,再去第一次打開網頁時,速度會很慢,因爲需要重新加載被踢出去的不活躍頁面。
**sudo swapoff –a**
**echo 1 > /proc/sys/vm/overcommit_memory**
**cat /proc/pid/smps >1**
**./oom.out**
**cat /proc/pid/smaps >2**
**meld 1 2**
可見命中的部分 page cached,被 LRU 踢走。
CPU>>mm>>io
嵌入式設備,一般不用 swap,不使能 swap,因爲:
-
- 嵌入式磁盤速度很慢;
-
2.FLASH 讀寫壽命有限;
zRAM
在物理內存劃分一部分,作爲 “swap” 分區,用來做頁面回收置換,利用強大 CPU 算力換取更大內存空間;
內核使能 swap
echo $((48*1024*1024)) > /sys/block/zram0/disksize
打開 swap 分區:
-
swapon –p 10 /dev/zram0
-
cat /proc/swaps
-
swapoff –a 不能關掉文件背景頁面。
-
不帶 pagecached 的 IO, direct IO。 在用戶態根據業務特點做 cached
-
類似於 CPU 跳過 cache,直接訪問 mem;
六、其他工程問題以及調優
6.1DMA 和 cache 一致性
(1) 不帶 CACHE
自己寫驅動,申請 DMA,可以用 Coherent DMA buffers
void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t
*dma_handle,gfp_t flag);
void dma_free_coherent (struct device *dev, size_t size,void *cpu_addr,
dma_addr_t dma_handle);
CMA 和此 API 自動融合,調用 dma_alloc_coherent() 將從 CMA 獲得內存,不帶 cache。
(2) 流式 DMA
操作其他進程的地址,比如 tcp/ip 報文的緩存,無法控制 DMA 地址源,用 dma_map_single,自動設置 cache flush,CPU 無法訪問 cache,這個是硬件自動完成的;但是 CPU 可以控制 cache,使能 cache 爲非法 (破壞命中),會自動與 memory 同步。
DMA Streaming Mapping
dma_addr_t dma_map_single(...);
void dma_unmap_single (...);
有的強大 DMA,不需要連續內存做 DMA 操作,scater/getter 可以用
int dma_map_sg(...);
void dma_unmap_sg (...);
有一些強大硬件,DMA 能感知 cache 網絡互聯;這 3 套 API,對任何硬件都成立;實現 API 後端,兼容不同硬件。
有些新的強大硬件,有支持 IOMMU/smmu
Dma 可以從不連續內存,CMA 申請內存 dma_alloc_coherent 不需要從 CMA 申請內存,支持 MMU,可以用物理不連續的內存來實現 DMA 操作;
6.2 內存的 cgroup
./swapoff –a
echo 1 \> /proc/sys/vm/overcommit_memory //
/sys/fs/cgroup/memory
mkdir A
cd A
sudo echo \$(100\*1024\*1024) \> memory.limit_in_bytes //限制最大內存100M
//a.out放到A cgroup執行;
sudo cgexec –g memory:A ./a.out
性能方面的調優:page in/out, swapin/out
6.3Dirty ratio 的一些設置
髒頁寫回
時間維度:時間到,髒頁寫回;dirty_expire_centisecs
空間維度:Dirty_ratio
Dirty_background_ratio:
cd /proc/sys/vm
假如某個進程在不停寫數據,當寫入大小觸發 dirty_background_ratio_10% 時,髒頁開始寫入磁盤,寫入數據大小觸發 dirty_ratio_20% 時 (磁盤 IO 速度遠比寫內存慢),如果繼續寫,會被內核阻塞,等髒頁部分被寫入磁盤,釋放 pagecached 後,進程才能繼續寫入內存;
所有的髒頁 flush,都是後臺自動完成,當寫入速度不足以觸發 dirty_ratio 時,進程感知不到磁盤的存在。
6.4swappiness
內存回收 reclaim,涉及三個水位
回收哪裏的內存, back_groudpages, swap,由 swappiness 決定
Swappiness 越大,越傾向於回收匿名頁;即使 Swappiness 設置爲 0,先回收背景頁,以使達到最低水位;假使達不到,還是會回收匿名頁;
有個例外,對於 cgroup 而言,設置 swappiness=0,該 group 的 swap 回收被關閉;但是全局的 swap 還是可以回收;
爲什麼設定最低水位,在 Linux 有一些緊急申請,PF_MEMALLOC,可以突破 min 水位,(回收內存的過程,也需要申請內存,類似徵糧隊自身需要喫糧食)。
最低水位,可以通過 lewmem_reserve_ratio 修改;
關於最低水位的計算方法:
回收 inode/dentry 的 slab
設置 drop_caches
getdelays 工具
getdelays 測量調度、I/O、SWAP、Reclaim 的延遲,需要內核打開相應選項
CONFIG_TASK_DELAY_ACCT=y
CONFIG_TASKSTATS=y
Documentation/accounting/delay-accounting.txt
vmstat
用法, man vmstate
模糊查找
apropos timer
(base) leon\@pc:\~/work/myHub/linux/kernel/linux/Documentation/accounting\$
apropos vmstat
vmstat (8) - Report virtual memory statistics
(base) leon\@pc:\~/work/myHub/linux/kernel/linux/Documentation/accounting\$
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/NcX9Tmlev5NMyjohysqVzg