記憶體管理子系統的架構如圖3。1所示,分為使用者空間、核心空間和硬體3個層面。
圖3。1 記憶體管理架構
1.使用者空間
應用程式使用malloc()申請記憶體,使用free()釋放記憶體。
malloc()和free()是glibc庫的記憶體分配器ptmalloc提供的介面,ptmalloc使用系統呼叫brk或mmap向核心以頁為單位申請記憶體,然後劃分成小記憶體塊分配給應用程式。
使用者空間的記憶體分配器,除了glibc庫的ptmalloc,還有谷歌公司的tcmalloc和FreeBSD的jemalloc。
2.核心空間
(1)核心空間的基本功能。
虛擬記憶體管理負責從程序的虛擬地址空間分配虛擬頁,sys_brk用來擴大或收縮堆,sys_mmap用來在記憶體對映區域分配虛擬頁,sys_munmap用來釋放虛擬頁。
核心使用延遲分配物理記憶體的策略,程序第一次訪問虛擬頁的時候,觸發頁錯誤異常,頁錯誤異常處理程式從頁分配器申請物理頁,在程序的頁表中把虛擬頁對映到物理頁。
頁分配器負責分配物理頁,當前使用的頁分配器是夥伴分配器。
核心空間提供了把頁劃分成小記憶體塊分配的塊分配器,提供分配記憶體的介面kmalloc()和釋放記憶體的介面kfree(),支援3種塊分配器:SLAB分配器、SLUB分配器和SLOB分配器。
在核心初始化的過程中,頁分配器還沒準備好,需要使用臨時的引導記憶體分配器分配記憶體。
(2)核心空間的擴充套件功能。
不連續頁分配器提供了分配記憶體的介面vmalloc和釋放記憶體的介面vfree,在記憶體碎片化的時候,申請連續物理頁的成功率很低,可以申請不連續的物理頁,對映到連續的虛擬頁,即虛擬地址連續而物理地址不連續。
每處理器記憶體分配器用來為每處理器變數分配記憶體。
連續記憶體分配器(Contiguous Memory Allocator,CMA)用來給驅動程式預留一段連續的記憶體,當驅動程式不用的時候,可以給程序使用;當驅動程式需要使用的時候,把程序佔用的記憶體透過回收或遷移的方式讓出來,給驅動程式使用。
記憶體控制組用來控制程序佔用的記憶體資源。
當記憶體碎片化的時候,找不到連續的物理頁,記憶體碎片整理(“memory compaction”的意譯,直譯為“記憶體緊縮”)透過遷移的方式得到連續的物理頁。
在記憶體不足的時候,頁回收負責回收物理頁,對於沒有後備儲存裝置支援的匿名頁,把資料換出到交換區,然後釋放物理頁;對於有後備儲存裝置支援的檔案頁,把資料寫回儲存裝置,然後釋放物理頁。如果頁回收失敗,使用最後一招:記憶體耗盡殺手(OOM killer,Out-of-Memory killer),選擇程序殺掉。
3.硬體層面
處理器包含一個稱為記憶體管理單元(Memory Management Unit,MMU)的部件,負責把虛擬地址轉換成物理地址。
記憶體管理單元包含一個稱為頁表快取(Translation Lookaside Buffer,TLB)的部件,儲存最近使用過的頁表對映,避免每次把虛擬地址轉換成物理地址都需要查詢記憶體中的頁表。
為了解決處理器的執行速度和記憶體的訪問速度不匹配的問題,在處理器和記憶體之間增加了快取。快取通常分為一級快取和二級快取,為了支援並行地取指令和取資料,一級快取分為資料快取和指令快取。
3.2 虛擬地址空間佈局
3.2.1 虛擬地址空間劃分
因為目前應用程式沒有那麼大的記憶體需求,所以ARM64處理器不支援完全的64位虛擬地址,實際支援情況如下。
(1)虛擬地址的最大寬度是48位,如圖3。2所示。核心虛擬地址在64位地址空間的頂部,高16位是全1,範圍是[0xFFFF 0000 0000 0000,0xFFFF FFFF FFFF FFFF];使用者虛擬地址在64位地址空間的底部,高16位是全0,範圍是[0x0000 0000 0000 0000,0x0000 FFFF FFFF FFFF];高16位是全1或全0的地址稱為規範的地址,兩者之間是不規範的地址,不允許使用。
(2)如果處理器實現了 ARMv8。2 標準的大虛擬地址(Large Virtual Address,LVA)支援,並且頁長度是64KB,那麼虛擬地址的最大寬度是52位。
(3)可以為虛擬地址配置比最大寬度小的寬度,並且可以為核心虛擬地址和使用者虛擬地址配置不同的寬度。轉換控制暫存器(Translation Control Register)TCR_EL1的欄位T0SZ定義了必須是全0的最高位的數量,欄位T1SZ定義了必須是全1的最高位的數量,使用者虛擬地址的寬度是(64-TCR_EL1。T0SZ),核心虛擬地址的寬度是(64-TCR_EL1。T1SZ)。
在編譯ARM64架構的Linux核心時,可以選擇虛擬地址寬度。
(1)如果選擇頁長度4KB,預設的虛擬地址寬度是39位。
(2)如果選擇頁長度16KB,預設的虛擬地址寬度是47位。
(3)如果選擇頁長度64KB,預設的虛擬地址寬度是42位。
(4)可以選擇48位虛擬地址。
在ARM64架構的Linux核心中,核心虛擬地址和使用者虛擬地址的寬度相同。
所有程序共享核心虛擬地址空間,每個程序有獨立的使用者虛擬地址空間,同一個執行緒組的使用者執行緒共享使用者虛擬地址空間,核心執行緒沒有使用者虛擬地址空間。
3.2.2 使用者虛擬地址空間佈局
程序的使用者虛擬地址空間的起始地址是0,長度是TASK_SIZE,由每種處理器架構定義自己的宏TASK_SIZE。ARM64架構定義的宏TASK_SIZE如下所示。
(1)32位使用者空間程式:TASK_SIZE的值是TASK_SIZE_32,即0x100000000,等於4GB。
(2)64位使用者空間程式:TASK_SIZE的值是TASK_SIZE_64,即2
VA_BITS
位元組,VA_BITS是編譯核心時選擇的虛擬地址位數。
arch/arm64/include/asm/memory。h
#define VA_BITS (CONFIG_ARM64_VA_BITS)
#define TASK_SIZE_64 (UL(1)
<< VA_BITS)
#ifdef CONFIG_COMPAT /* 支援執行32位使用者空間程式 */
#define TASK_SIZE_32 UL(0x100000000)
/* test_thread_flag(TIF_32BIT)判斷使用者空間程式是不是32位 */
#define TASK_SIZE (test_thread_flag(TIF_32BIT)
? \
TASK_SIZE_32 : TASK_SIZE_64)
#define TASK_SIZE_OF(tsk)
(test_tsk_thread_flag(tsk, TIF_32BIT)
? \
TASK_SIZE_32 : TASK_SIZE_64)
#else
#define TASK_SIZE TASK_SIZE_64
#endif
/* CONFIG_COMPAT */
程序的使用者虛擬地址空間包含以下區域。
(1)程式碼段、資料段和未初始化資料段。
(2)動態庫的程式碼段、資料段和未初始化資料段。
(3)存放動態生成的資料的堆。
(4)存放區域性變數和實現函式呼叫的棧。
(5)存放在棧底部的環境變數和引數字串。
(6)把檔案區間對映到虛擬地址空間的記憶體對映區域。
核心使用記憶體描述符mm_struct描述程序的使用者虛擬地址空間,記憶體描述符的主要成員如表3。1所示。
表3。1 記憶體描述符的主要成員
程序描述符(task_struct)中和記憶體描述符相關的成員如表3。2所示。
表3。2 程序描述符中和記憶體描述符相關的成員
如果程序不屬於執行緒組,那麼程序描述符和記憶體描述符的關係如圖 3。3 所示,程序描述符的成員mm和active_mm都指向同一個記憶體描述符,記憶體描述符的成員mm_users是1、成員mm_count是1。
如果兩個程序屬於同一個執行緒組,那麼程序描述符和記憶體描述符的關係如圖3。4所示,每個程序的程序描述符的成員mm和active_mm都指向同一個記憶體描述符,記憶體描述符的成員mm_users是2、成員mm_count是1。
圖3。3 程序的程序描述符和記憶體描述符的關係 圖3。4 執行緒組的程序描述符和記憶體描述符的關係
核心執行緒的程序描述符和記憶體描述符的關係如圖 3。5 所示,核心執行緒沒有使用者虛擬地址空間,當核心執行緒沒有執行的時候,程序描述符的成員mm和active_mm都是空指標;當核心執行緒執行的時候,借用上一個程序的記憶體描述符,在被借用程序的使用者虛擬地址空間的上方執行,程序描述符的成員active_mm指向借用的記憶體描述符,假設被借用的記憶體描述符所屬的程序不屬於執行緒組,那麼記憶體描述符的成員mm_users不變,仍然是1,成員mm_count加1變成2。
圖3。5 核心執行緒的程序描述符和記憶體描述符的關係
為了使緩衝區溢位攻擊更加困難,核心支援為記憶體對映區域、棧和堆選擇隨機的起始地址。程序是否使用虛擬地址空間隨機化的功能,由以下兩個因素共同決定。
(1)程序描述符的成員personality(個性化)是否設定ADDR_NO_RANDOMIZE。
(2)全域性變數randomize_va_space:0表示關閉虛擬地址空間隨機化,1表示使記憶體對映區域和棧的起始地址隨機化,2表示使記憶體對映區域、棧和堆的起始地址隨機化。可以透過檔案“/proc/sys/kernel/randomize_va_space”修改。
mm/memory。c
int randomize_va_space __read_mostly =
#ifdef CONFIG_COMPAT_BRK
1;
#else
2;
#endif
為了使舊的應用程式(基於libc5)正常執行,預設開啟配置宏CONFIG_COMPAT_BRK,禁止堆隨機化。所以預設配置是使記憶體對映區域和棧的起始地址隨機化。
棧通常自頂向下增長,當前只有惠普公司的PA-RISC處理器的棧是自底向上增長。棧的起始地址是STACK_TOP,預設啟用棧隨機化,需要把起始地址減去一個隨機值。STACK_TOP是每種處理器架構自定義的宏,ARM64架構定義的STACK_TOP如下所示:如果是64位使用者空間程式,STACK_TOP的值是TASK_SIZE_64;如果是32位使用者空間程式,STACK_TOP的值是異常向量的基準地址0xFFFF0000。
arch/arm64/include/asm/processor。h
#define STACK_TOP_MAX TASK_SIZE_64
#ifdef CONFIG_COMPAT /* 支援執行32位使用者空間程式 */
#define AARCH32_VECTORS_BASE 0xffff0000
#define STACK_TOP (test_thread_flag(TIF_32BIT)
? \
AARCH32_VECTORS_BASE : STACK_TOP_MAX)
#else
#define STACK_TOP STACK_TOP_MAX
#endif
/* CONFIG_COMPAT */
記憶體對映區域的起始地址是記憶體描述符的成員 mmap_base。如圖 3。6 所示,使用者虛擬地址空間有兩種佈局,區別是記憶體對映區域的起始位置和增長方向不同。
(1)傳統佈局:記憶體對映區域自底向上增長,起始地址是TASK_UNMAPPED_BASE,每種處理器架構都要定義這個宏,ARM64架構定義為 TASK_SIZE/4。預設啟用記憶體對映區域隨機化,需要把起始地址加上一個隨機值。傳統佈局的缺點是堆的最大長度受到限制,在32位系統中影響比較大,但是在64位系統中這不是問題。
(2)新佈局:記憶體對映區域自頂向下增長,起始地址是(STACK_TOP − 棧的最大長度 − 間隙)。預設啟用記憶體對映區域隨機化,需要把起始地址減去一個隨機值。
當程序呼叫execve以裝載ELF檔案的時候,函式load_elf_binary將會建立程序的使用者虛擬地址空間。函式load_elf_binary建立使用者虛擬地址空間的過程如圖3。7所示。
如果沒有給程序描述符的成員personality設定標誌位ADDR_NO_RANDOMIZE(該標誌位表示禁止虛擬地址空間隨機化),並且全域性變數randomize_va_space是非零值,那麼給程序設定標誌PF_RANDOMIZE,允許虛擬地址空間隨機化。
圖3。6 使用者虛擬地址空間的兩種佈局
圖3。7 裝載ELF檔案時建立虛擬地址空間
各種處理器架構自定義的函式arch_pick_mmap_layout負責選擇記憶體對映區域的佈局。ARM64架構定義的函式arch_pick_mmap_layout如下:
arch/arm64/mm/mmap。c
1
void arch_pick_mmap_layout(struct mm_struct *mm)
2
{
3
unsigned
long random_factor =
0UL;
4
5
if
(current->flags & PF_RANDOMIZE)
6 random_factor = arch_mmap_rnd();
7
8
if
(mmap_is_legacy())
{
9 mm->mmap_base = TASK_UNMAPPED_BASE + random_factor;
10 mm->get_unmapped_area = arch_get_unmapped_area;
11
}
else
{
12 mm->mmap_base = mmap_base(random_factor);
13 mm->get_unmapped_area = arch_get_unmapped_area_topdown;
14
}
15
}
16
17
static
int mmap_is_legacy(void)
18
{
19
if
(current->personality & ADDR_COMPAT_LAYOUT)
20
return
1;
21
22
if
(rlimit(RLIMIT_STACK)
== RLIM_INFINITY)
23
return
1;
24
25
return sysctl_legacy_va_layout;
26
}
第8~10行程式碼,如果給程序描述符的成員personality設定標誌位ADDR
COMPAT
LAYOUT表示使用傳統的虛擬地址空間佈局,或者使用者棧可以無限增長,或者透過檔案“/proc/sys/vm/legacy_va_layout”指定,那麼使用傳統的自底向上增長的佈局,記憶體對映區域的起始地址是 TASK_UNMAPPED_BASE 加上隨機值,分配未對映區域的函式是arch_get_unmapped_area。
第11~13行程式碼,如果使用自頂向下增長的佈局,那麼分配未對映區域的函式是arch_ get_unmapped_area_topdown,記憶體對映區域的起始地址的計算方法如下:
arch/arm64/include/asm/elf。h
#ifdef CONFIG_COMPAT
#define STACK_RND_MASK (test_thread_flag(TIF_32BIT)
? \
0x7ff
>>
(PAGE_SHIFT -
12)
: \
0x3ffff
>>
(PAGE_SHIFT -
12))
#else
#define STACK_RND_MASK (0x3ffff
>>
(PAGE_SHIFT -
12))
#endif
arch/arm64/mm/mmap。c
#define MIN_GAP (SZ_128M
+
((STACK_RND_MASK << PAGE_
#define MAX_GAP (STACK_TOP/6*5)
static
unsigned
long mmap_base(unsigned
long rnd)
{
unsigned
long gap = rlimit(RLIMIT_STACK);
if
(gap < MIN_GAP)
gap = MIN_GAP;
else
if
(gap > MAX_GAP)
gap = MAX_GAP;
return PAGE_ALIGN(STACK_TOP - gap - rnd);
}
先計算記憶體對映區域的起始地址和棧頂的間隙:初始值取使用者棧的最大長度,限定不能小於“128MB + 棧的最大隨機偏移值 + 1”,確保使用者棧最大可以達到128MB;限定不能超過STACK_TOP的5/6。記憶體對映區域的起始地址等於“STACK_TOP−間隙−隨機值”,然後向下對齊到頁長度。
回到函式load_elf_binary:函式setup_arg_pages把棧頂設定為STACK_TOP減去隨機值,然後把環境變數和引數從臨時棧移到最終的使用者棧;函式set_brk設定堆的起始地址,如果啟用堆隨機化,把堆的起始地址加上隨機值。
fs/binfmt_elf。c
static
int load_elf_binary(struct linux_binprm *bprm)
{
…
retval = setup_arg_pages
(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
…
retval = set_brk(elf_bss, elf_brk, bss_prot);
…
if
((current->flags & PF_RANDOMIZE)
&&
(randomize_va_space >
1))
{
current->mm->brk = current->mm->start_brk =
arch_randomize_brk(current->mm);
}
…
}
3.2.3 核心地址空間佈局
ARM64處理器架構的核心地址空間佈局如圖3。8所示。
圖3。8 ARM64架構的核心地址空間佈局
(1)線性對映區域的範圍是[PAGE_OFFSET, 2
64
−1],起始位置是PAGE_OFFSET = (0xFFFF FFFF FFFF FFFF << (VA_BITS-1)),長度是核心虛擬地址空間的一半。稱為線性對映區域的原因是虛擬地址和物理地址是線性關係:
虛擬地址 =((物理地址 − PHYS_OFFSET)+ PAGE_OFFSET),其中PHYS_OFFSET是記憶體的起始物理地址。
(2)vmemmap 區域的範圍是[VMEMMAP_START, PAGE_OFFSET),長度是VMEMMAP_SIZE =(線性對映區域的長度 / 頁長度 * page結構體的長度上限)。
核心使用page結構體描述一個物理頁,記憶體的所有物理頁對應一個page結構體陣列。如果記憶體的物理地址空間不連續,存在很多空洞,稱為稀疏記憶體。vmemmap區域是稀疏記憶體的page結構體陣列的虛擬地址空間。
(3)PCI I/O區域的範圍是[PCI_IO_START, PCI_IO_END),長度是16MB,結束地址是PCI_IO_END = (VMEMMAP_START − 2MB)。
外圍元件互聯(Peripheral Component Interconnect,PCI)是一種匯流排標準,PCI I/O區域是PCI裝置的I/O地址空間。
(4)固定對映區域的範圍是[FIXADDR_START, FIXADDR_TOP),長度是FIXADDR_SIZE,結束地址是FIXADDR_TOP = (PCI_IO_START − 2MB)。
固定地址是編譯時的特殊虛擬地址,編譯的時候是一個常量,在核心初始化的時候對映到物理地址。
(5) vmalloc區域的範圍是[VMALLOC
START, VMALLOC_END),起始地址是VMALLOC
START,等於核心模組區域的結束地址,結束地址是VMALLOC_END = (PAGE_OFFSET − PUD_SIZE − VMEMMAP_SIZE − 64KB),其中PUD_SIZE是頁上級目錄表項對映的地址空間的長度。
vmalloc區域是函式vmalloc使用的虛擬地址空間,核心使用vmalloc分配虛擬地址連續但物理地址不連續的記憶體。
核心映象在vmalloc區域,起始虛擬地址是(KIMAGE_VADDR + TEXT_OFFSET) ,其中KIMAGE_VADDR是核心映象的虛擬地址的基準值,等於核心模組區域的結束地址MODULES_END;TEXT_OFFSET是記憶體中的核心映象相對記憶體起始位置的偏移。
(6)核心模組區域的範圍是[MODULES_VADDR, MODULES_END),長度是128MB,起始地址是MODULES_VADDR =(核心虛擬地址空間的起始地址 + KASAN影子區域的長度)。
核心模組區域是核心模組使用的虛擬地址空間。
(7)KASAN影子區域的起始地址是核心虛擬地址空間的起始地址,長度是核心虛擬地址空間長度的1/8。
核心地址消毒劑(Kernel Address SANitizer,KASAN)是一個動態的記憶體錯誤檢查工具。它為發現釋放後使用和越界訪問這兩類缺陷提供了快速和綜合的解決方案。
本文摘自《Linux核心深度解析 》,作者: 餘華兵
編輯推薦
基於ARM64架構的Linux 4。x核心
全面介紹核心引導、程序管理、記憶體管理、異常處理、互斥技術和檔案系統等關鍵子系統的實現。
核心引導部分詳解從處理器上電到使用者空間的程序產生的整個過程,並介紹多處理器系統的啟動過程。
結合原始碼分析,詳細解讀每種技術的使用方法及其原理。
透過圖例幫助讀者理解各種資料結構之間的關係。
透過執行流程圖幫助讀者理解函式的執行過程。
對同類技術進行歸納總結和對比分析,例如3種塊分配器、巨型頁的兩種實現、解決記憶體碎片問題的各種技術、3種中斷下半部和3種RCU技術,等等。