記憶體功能
| 記憶體 |
|---|
| 記憶體訪問 |
| 虛擬記憶體 |
| 記憶體對映 |
| 按需分頁和交換 |
| 邏輯記憶體 |
| 頁面分配器 |
| 頁面 |
核心可以完全訪問系統的記憶體,並允許程序安全地訪問這些記憶體,以滿足它們的需要。通常,第一步是使用虛擬定址,通常透過分頁和/或分段實現。虛擬定址允許核心使給定的物理地址看起來像另一個地址,即虛擬地址。對於不同的程序,虛擬地址空間可能不同;一個程序在特定(虛擬)地址訪問的記憶體可能與另一個程序在同一地址訪問的記憶體不同。這允許每個程式都表現得像它唯一執行的程式一樣(除了核心),從而防止應用程式相互崩潰。
在許多系統上,程式的虛擬地址可能引用當前不在記憶體中的資料。虛擬定址提供的間接層允許作業系統使用其他資料儲存(如硬碟驅動器)來儲存那些原本必須保留在主隨機存取記憶體(RAM)中的資料。因此,作業系統可以允許程式使用比系統實際可用的記憶體更多的記憶體。當程式需要當前不在 RAM 中的資料時,MMU會向核心發出訊號,指示發生了這種情況,核心會相應地將非活動記憶體塊的內容寫入磁碟(如有必要),並用程式請求的資料替換它。然後,程式可以從它停止的位置恢復執行。這種方案通常稱為按需分頁。
虛擬定址還允許在兩個不相交區域建立虛擬記憶體分割槽,一個區域保留給核心(核心空間),另一個區域保留給應用程式(使用者空間)。處理器不允許應用程式訪問核心記憶體,從而防止應用程式損壞正在執行的核心。這種基本的記憶體空間分割槽極大地促進了實際通用核心的當前設計,並且在這些系統中幾乎是通用的,Linux 就是其中之一。
⚲ Shell 介面
- cat /proc/meminfo
- man 1 free
- man 8 vmstat
- ⚲ man 2 brk ↪ sys_brk id, do_brk_flags id 動態更改呼叫程序的資料段大小。
更改是透過重置程序的程式斷點來完成的,程式斷點決定了可以分配的最大空間。程式斷點是資料區域當前末尾之後的第一個位置的地址,它決定了程序可以分配的最大空間。隨著斷點值增加,可用空間量也會增加。新增的可用空間被初始化為零值。
- ⚲ man 2 mmap ↪ ksys_mmap_pgoff id 將檔案或裝置對映到記憶體。
它是記憶體對映檔案 I/O 的一種方法。它自然地實現了按需分頁,因為檔案內容最初沒有從磁碟讀取,並且根本不使用物理 RAM。實際的磁碟讀取是在訪問特定位置後以“延遲”方式執行的。在不再需要記憶體後,重要的是要man 2 unmmap 指向它的指標。可以使用man 2 mprotect ↪ do_mprotect_pkey id 管理保護資訊,並且可以使用man 2 madvise ↪ do_madvise id 強制執行特殊處理。在 Linux 中,man 2 mmap 可以建立幾種型別的對映,例如匿名對映、共享對映和私有對映。使用MAP_ANONYMOUS 標誌mmap()可以對映程序虛擬記憶體中的特定區域,該區域不受任何檔案的支援,其內容被初始化為零。
這些函式通常從更高層的記憶體管理庫函式呼叫,例如 C 標準庫man 3 malloc 或C++ new 運算子。
💾 歷史:Linux 從 Unix 繼承了兩個與記憶體管理系統呼叫相關的基本呼叫:brk 和 mmap。
BTW:在 Linux 中,man 2 sbrk 不是單獨的系統呼叫,而是一個 C 庫函式,它也呼叫sys_brk id 並保持一些內部狀態以返回之前的斷點值。
📚 參考文獻
⚙️ 內部機制
🔧 TODO
🗝️ 首字母縮略詞
- VPFN - 虛擬頁面幀編號
- PFN - 物理頁面幀編號
- pgd - 頁面目錄
- pmd - 頁面中間目錄
- pud - 頁面上層目錄
- pte - 頁表條目
- TLB - 轉換後備緩衝器
- MMU - 記憶體管理單元
⚲ API
⚙️ 內部機制
📚 參考文獻
⚲ API
- linux/types.hinc
- linux/kref.hinc
- list_head id - 通用雙向連結串列
- linux/list.h inc - 基本 list_head id 操作
- linux/klist.h inc - 一些 klist_node id->kref id 輔助函式
- klist_add_tailid ...
- linux/kobject.hinc
- linux/circ_buf.hinc
- linux/kfifo.h inc - 通用核心 FIFO
- kfifo_inid ...
- linux/rbtree.h inc - 紅黑樹
- linux/scatterlist.hinc
- linux/idr.h inc - ID 分配
- linux/bitmap.hinc
📚 參考文獻
記憶體對映
[edit | edit source]🔧 TODO
關鍵專案
man 2 mmap man 2 mprotect man 2 mmap2 man 2 mincore man 2 ksys_mmap_pgoff
do_mmap id mm_struct id vm_area_struct id vm_struct id remap_pfn_range id SetPageReserved id ClearPageReserved id free_mmap_pages alloc_mmap_pages free_mmap_pages id
⚲ API
⚙️ 內部機制
📚 參考文獻
交換
[edit | edit source]🔧 TODO
⚲ API
- cat /proc/sys/vm/swappiness ↪ vm_swappiness id
- linux/swap.hinc
- man 2 swapon ↪ enable_swap_slots_cache id
- man 2 swapoff
- man 2 mlock ↪ do_mlock id
- man 2 shmctl ↪ shmctl_do_lock id
⚙️ 內部機制
VM_LOCKED id swap_info_struct id si_swapinfo id swap_info id handle_pte_fault id do_swap_page id wakeup_kswapd id kswapd id
📚 參考文獻
邏輯記憶體
[edit | edit source]⚲ kmalloc id 是核心中為小於頁面大小的物件分配記憶體的常用方法。它在 linux/slab.h inc 中定義。第一個引數 size 是要分配的記憶體塊的大小(以位元組為單位)。第二個引數 flags 是分配標誌或 GFP 標誌,它是一組宏,呼叫者透過它們來控制所請求記憶體的型別。最常用的 flags 值是 GFP_KERNEL 和 GFP_ATOMIC,但還有更多需要考慮的因素。
核心中的記憶體分配請求始終由一組 GFP 標誌(“GFP” 最初來自“get free page”)限定,這些標誌描述了為了滿足請求可以和不可以做什麼。最常用的標誌是 GFP_ATOMIC 和 GFP_KERNEL,儘管它們實際上是由更底層的標誌構建的。完整的標誌集非常龐大;它們可以在 linux/gfp.h inc 標頭檔案中找到。
⚲ API
- ↯ RAII 分配函式層次結構來自 linux/device.h inc
- devm_kcalloc id - 清零陣列
- devm_kzalloc id - 清零分配
- devm_kmalloc id - 通用分配
- 經典直接 API
Slab 分配
[edit | edit source]Slab 分配 是一種記憶體管理演算法,旨在有效地為核心物件分配記憶體。它消除了由分配和釋放導致的碎片。該技術用於保留分配的記憶體,其中包含特定型別的資料物件,以便在隨後分配相同型別物件的分配中重新使用。
基礎知識
本節介紹 SLAB 和 SLUB 分配器實現
可以將 slab 想象成一個跨越一個或多個連續記憶體頁的、包含特定型別或大小物件的陣列;例如,名為“task_struct”的 slab 包含 struct task_struct 型別的物件,供排程子系統使用。其他 slab 儲存其他子系統使用的物件,還有一些 slab 用於核心內部的動態分配,例如“kmalloc-64” slab,它儲存透過 kmalloc() 呼叫請求的最多 64 位元組的塊。在一個 slab 中,每個物件可以單獨分配和釋放。
slab 分配的主要動機是,核心資料物件的初始化和銷燬實際上可能超過為它們分配記憶體的成本。由於物件建立和刪除被核心廣泛使用,初始化的開銷可能會導致效能大幅下降。因此引入了物件快取的概念,以避免呼叫用於初始化物件狀態的函式。
使用 slab 分配,會預先分配適合容納特定型別或大小的資料物件的記憶體塊。slab 分配器會跟蹤這些塊,這些塊被稱為快取 kmalloc_caches id,因此當收到為特定型別的資料物件分配記憶體的請求時,它可以使用已經分配的插槽 slab_alloc id 立即滿足請求。
使用 kfree id 釋放物件不會釋放記憶體,而只是開啟一個插槽,該插槽由 slab 分配器放入空閒插槽列表 kmem_cache_cpu id 中。下次呼叫分配相同大小的記憶體時,將返回現在未使用的記憶體插槽。參見 slab_alloc id//___slab_alloc id/get_freelist id。此過程消除了搜尋合適記憶體空間的需要,並極大地緩解了記憶體碎片。在這種情況下,slab 是記憶體中包含預先分配的記憶體塊的一個或多個連續頁。
slab 分配為核心中那些需要比標準 4KB 頁大小更靈活的記憶體分配的部分提供了一種面向 zoned buddy 分配器的前端。
⚲ 介面
- sudo cat /proc/slabinfo
- linux/slab.hinc
- kmem_cache_alloc id, kmem_cache_free id
- man 1 slabtop
⚙️ 內部機制
SLUB 分配器 – 預設的非排隊分配器
SLUB 是原始 SLAB 分配器的迭代版本,它取代了原始 SLAB 分配器,並從 2.6.23 版本起成為 Linux 的預設分配器。
⚙️ 內部實現: mm/slub.c src
📚 參考文獻
SLOB 分配器 – 用於 🤖 嵌入式裝置的簡單塊列表
不幸的是,SLAB 和 SLUB 分配器會消耗大量記憶體來分配它們的 slab,這對記憶體受限的小型系統(如嵌入式系統)來說是一個嚴重的缺點。為了克服這個問題,Matt Mackall 在 2006 年 1 月設計了 SLOB(簡單塊列表)分配器,它是一種更簡單的分配核心物件的方法。
SLOB 分配器使用首次適應演算法,它選擇第一個可用的空間作為記憶體。此演算法減少了記憶體消耗,但此方法的一個主要限制是它非常容易受到內部碎片的影響。
當沒有定義 slab 分配器時(當 CONFIG_SLAB id 標誌被停用時),核心構建系統也會使用 SLOB 分配器作為後備。
⚙️ 內部實現: mm/slob.c src, slob_alloc id
SLAB 分配器
💾 歷史:SLAB 分配器是核心中第一個 slab 分配實現的名稱,用於將其與使用相同介面的後續分配器區分開來。它很大程度上基於 Jeff Bonwick 的論文“The Slab Allocator: An Object-Caching Kernel Memory Allocator”(1994),該論文描述了在 Solaris 5.4 核心中實現的第一個 slab 分配器。
SLAB 是賦予核心中第一個 slab 分配實現的名稱,用於將其與之後使用相同介面的分配器區分開來。它很大程度上基於 Jeff Bonwick 的論文“The Slab Allocator: An Object-Caching Kernel Memory Allocator”(1994 年),該論文描述了在 Solaris 5.4 核心中實現的第一個 slab 分配器。
⚙️ 內部實現: mm/slab.c src
📚 Slab 分配的參考資料
- KASAN - KernelAddressSANitizer doc - 用於查詢越界和使用後釋放錯誤的動態記憶體安全錯誤檢測器
- 影片“SL[AUO]B:核心記憶體分配器設計和理念” Christopher Lameter (Linux.conf.au 2015 大會) 幻燈片
頁面分配器
[edit | edit source]頁分配器(或“zoned buddy 分配器”)是一個處理物理記憶體的底層分配器。它將物理頁(通常大小為 4096 位元組)的空閒記憶體交付給高階記憶體使用者,例如 slab 分配器和 kmalloc()。作為系統中記憶體的最終來源,頁分配器必須確保始終有可用記憶體,因為無法為關鍵核心子系統提供記憶體可能會導致系統整體故障或核心崩潰。
頁分配器將物理記憶體劃分為“區域”,每個區域對應於 zone_type id,具有特定的特徵。ZONE_DMA 包含地址範圍底部的記憶體,供嚴重受限的裝置使用,例如,而 ZONE_NORMAL id 可能包含系統中的大部分記憶體。32 位系統有一個 ZONE_HIGHMEM 用於未直接對映到核心地址空間的記憶體。根據任何給定分配請求的特性,頁分配器將按照特定優先順序順序搜尋可用的區域。對於好奇的人來說,/proc/zoneinfo提供了有關任何給定系統上正在使用的區域的大量資訊。
在一個區域內,記憶體被分組為頁塊,每個塊可以使用一個遷移型別進行標記 - migratetype id 描述了塊的分配方式。
⚲ API
- cat /proc/buddyinfo
- linux/gfp.hinc
- linux/mmzone.hinc
- alloc_pageid
- devm_get_free_pages id - RAII 函式,↯ 其層次結構
- __get_free_pagesid
- alloc_pagesid
- alloc_pages_nodeid
- __alloc_pages id - zoned buddy 分配器的“核心”
- alloc_pages_nodeid
- alloc_pagesid
- __get_free_pagesid
⚙️ 內部機制
- build_all_zonelists id 從 start_kernel id 中呼叫,↯ 呼叫層次結構
- __alloc_pages id - zoned buddy 分配器的“核心”
- struct zone id
- mm/mmzone.csrc
- mm/page_alloc.csrc
📚 參考文獻
📚 邏輯記憶體的參考資料
物理記憶體
[edit | edit source]記憶體佈局
[edit | edit source]32 位處理器最多可以定址 4GB 記憶體。Linux 核心將 4GB 地址空間劃分為使用者程序和核心;在最常見的配置下,32 位範圍的前 3GB 劃歸使用者空間,核心從 0xc0000000 開始獲取最後的 1GB。共享地址空間提供了許多效能優勢;特別是,硬體的地址轉換緩衝區可以在核心和使用者空間之間共享。
在 x86-64 架構下 - CONFIG_X86_64 id,使用 4 級頁表 (CONFIG_X86_5LEVEL id=n) 時,虛擬記憶體地址的低 48 位將用於地址轉換(頁表查詢)。虛擬地址的第 48 到 63 位必須是第 47 位的副本,否則處理器將引發異常。符合此規則的地址被稱為“規範形式”。規範形式地址範圍從 0 到 00007FFF'FFFFFFFF,以及 FFFF8000'00000000 到 FFFFFFFF'FFFFFFFF,總共可以使用 256 TB 的 虛擬地址空間。這仍然大約是 32 位機器上虛擬地址空間的 64,000 倍。
Linux 將地址空間的較高部分用於核心空間,並將較低部分留給使用者空間。“規範地址”設計實際上有兩個記憶體部分:較低部分從 00000000'00000000 開始並向上擴充套件,隨著更多虛擬地址位可用而擴充套件,而較高部分則停靠在地址空間頂部並向下擴充套件。
| 起始地址 | 偏移量 | 結束地址 | 大小 | VM 區域描述 |
|---|---|---|---|---|
0000'8000'0000'0000
|
+128 TB | ffff'7fff'ffff'ffff
|
... 大量接近 64 位寬的非規範虛擬記憶體地址孔洞,一直延伸到核心對映的 -128 TB 起始偏移量。 | |
0000'0000'0000'0000
|
0 | 0000'7fff'ffff'ffff
|
128 TB=247 | 使用者空間虛擬記憶體,每個 mm 不同 |
ffff'ffff'ffe0'0000
|
-2 MB | ffff'ffff'ffff'ffff
|
2 MB=221 | ... 未使用的孔洞 |
ffff'ffff'ff60'0000
|
-10 MB | ffff'ffff'ff60'0fff
|
4 kB=212 | VSYSCALL_ADDR id - 遺留 vsyscall ABI |
ffff'ffff'8000'0000
|
-2 GB | ffff'ffff'9fff'ffff
|
512 MB=219 | 核心文字對映,對映到物理地址 0 |
ffff'8880'0000'0000
|
-119.5 TB | ffff'c87f'ffff'ffff
|
64 TB | page_offset_base id = __PAGE_OFFSET_BASE_L4 id - 所有物理記憶體的直接對映 |
ffff'8000'0000'0000
|
-128 TB | ffff'87ff'ffff'ffff
|
8 TB | ... 保護孔洞,也保留給虛擬機器管理程式 |

⚲ API
- man 8 setarch --addr-no-randomize cat /proc/self/maps
⚙️ 內部機制
📚 參考文獻
頁面
[edit | edit source]在 Linux 中,不同的架構具有不同的頁面大小。x86 架構的原始頁面大小(也是最常用的頁面大小)為 4096 位元組 (4 KB)。當前架構的頁面大小(以位元組為單位)由 PAGE_SIZE 宏定義,該宏包含在 arch/x86/include/asm/page_types.h src 標頭檔案中。使用者空間程式可以使用 man 2 getpagesize 庫函式獲取此值。另一個相關的宏是 PAGE_SHIFT,它包含將地址左移以獲取其頁號所需的位數 - 對於 4K 頁面,為 12 位。
與記憶體管理相關的最基本核心資料結構之一是 struct page。核心使用這種型別的變數來跟蹤系統中存在的每個物理記憶體頁面的狀態。現代系統中存在數百萬個頁面,因此記憶體中也存在數百萬個此類結構。
struct page 的完整定義可以在 linux/mm_types.h inc 中找到。
DMA
[edit | edit source]⚲ API
- dma_addr_t id - 匯流排地址
- linux/dma-mapping.hinc
- dma_alloc_coherentid
- dma_alloc_pages id pin_user_pages id
- dma_map_single id dma_data_direction id
- dma_map_sg id scatterlist id
- dma_set_mask id dma_set_coherent_mask id dma_set_mask_and_coherent id
- dma_sync_single_for_cpu id dma_sync_single_for_device id
- linux/gfp.hinc
- linux/dmapool.hinc
- dma_pool_createid
- 可 DMA 的記憶體:__get_free_page id kmalloc id kmem_cache_alloc id
- get_user_pages id 將使用者頁面固定到記憶體中。
👁 示例
⚙️ 內部機制
📚 參考文獻
💾 歷史記錄:
SAC 單地址週期
DMAEngine
[edit | edit source]- linux/dmaengine.hinc
- drivers/dmasrc
- 驅動程式 API/dmaengine doc
- https://bootlin.com/pub/conferences/2015/elc/ripard-dmaengine/
...
[edit | edit source]📚 文章參考資料