內存管理:虛擬地址空間布局
內存管理子系統的架構如圖1.1所示,分為用戶空間、內核空間和硬件3個層面。
圖1.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)的部件,保存最近使用過的頁表映射,避免每次把虛擬地址轉換成物理地址都需要查詢內存中的頁表。
為了解決處理器的執行速度和內存的訪問速度不匹配的問題,在處理器和內存之間增加了緩存。緩存通常分為一級緩存和二級緩存,為了支持并行地取指令和取數據,一級緩存分為數據緩存和指令緩存。
1.2 虛擬地址空間布局
1.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內核中,內核虛擬地址和用戶虛擬地址的寬度相同。
所有進程共享內核虛擬地址空間,每個進程有獨立的用戶虛擬地址空間,同一個線程組的用戶線程共享用戶虛擬地址空間,內核線程沒有用戶虛擬地址空間。
1.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,即2VA_BITS字節,VA_BITS是編譯內核時選擇的虛擬地址位數。
arch/arm64/include/asm/memory.h#define?VA_BITS??????????(CONFIG_ARM64_VA_BITS)#define?TASK_SIZE_64?????(UL(1)?<
進程的用戶虛擬地址空間包含以下區域。
(1)代碼段、數據段和未初始化數據段。
(2)動態庫的代碼段、數據段和未初始化數據段。
(3)存放動態生成的數據的堆。
(4)存放局部變量和實現函數調用的棧。
(5)存放在棧底部的環境變量和參數字符串。
(6)把文件區間映射到虛擬地址空間的內存映射區域。
內核使用內存描述符mm_struct描述進程的用戶虛擬地址空間,內存描述符的主要成員如表1.1所示。
表3.1 內存描述符的主要成員
|
進程描述符(task_struct)中和內存描述符相關的成員如表3.2所示。
表1.2 進程描述符中和內存描述符相關的成員
如果進程不屬于線程組,那么進程描述符和內存描述符的關系如圖 1.3 所示,進程描述符的成員mm和active_mm都指向同一個內存描述符,內存描述符的成員mm_users是1、成員mm_count是1。
如果兩個進程屬于同一個線程組,那么進程描述符和內存描述符的關系如圖1.4所示,每個進程的進程描述符的成員mm和active_mm都指向同一個內存描述符,內存描述符的成員mm_users是2、成員mm_count是1。
圖1.3 進程的進程描述符和內存描述符的關系 圖1.4 線程組的進程描述符和內存描述符的關系
內核線程的進程描述符和內存描述符的關系如圖 3.5 所示,內核線程沒有用戶虛擬地址空間,當內核線程沒有運行的時候,進程描述符的成員mm和active_mm都是空指針;當內核線程運行的時候,借用上一個進程的內存描述符,在被借用進程的用戶虛擬地址空間的上方運行,進程描述符的成員active_mm指向借用的內存描述符,假設被借用的內存描述符所屬的進程不屬于線程組,那么內存描述符的成員mm_users不變,仍然是1,成員mm_count加1變成2。
圖1.5 內核線程的進程描述符和內存描述符的關系
為了使緩沖區溢出攻擊更加困難,內核支持為內存映射區域、棧和堆選擇隨機的起始地址。進程是否使用虛擬地址空間隨機化的功能,由以下兩個因素共同決定。
(1)進程描述符的成員personality(個性化)是否設置ADDR_NO_RANDOMIZE。
(2)全局變量randomize_va_space:0表示關閉虛擬地址空間隨機化,1表示使內存映射區域和棧的起始地址隨機化,2表示使內存映射區域、棧和堆的起始地址隨機化。可以通過文件“/proc/sys/kernel/randomize_va_space”修改。
mm/memory.cint?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。如圖 1.6 所示,用戶虛擬地址空間有兩種布局,區別是內存映射區域的起始位置和增長方向不同。
(1)傳統布局:內存映射區域自底向上增長,起始地址是TASK_UNMAPPED_BASE,每種處理器架構都要定義這個宏,ARM64架構定義為 TASK_SIZE/4。默認啟用內存映射區域隨機化,需要把起始地址加上一個隨機值。傳統布局的缺點是堆的最大長度受到限制,在32位系統中影響比較大,但是在64位系統中這不是問題。
(2)新布局:內存映射區域自頂向下增長,起始地址是(STACK_TOP ? 棧的最大長度 ? 間隙)。默認啟用內存映射區域隨機化,需要把起始地址減去一個隨機值。
當進程調用execve以裝載ELF文件的時候,函數load_elf_binary將會創建進程的用戶虛擬地址空間。函數load_elf_binary創建用戶虛擬地址空間的過程如圖1.7所示。
如果沒有給進程描述符的成員personality設置標志位ADDR_NO_RANDOMIZE(該標志位表示禁止虛擬地址空間隨機化),并且全局變量randomize_va_space是非零值,那么給進程設置標志PF_RANDOMIZE,允許虛擬地址空間隨機化。
圖1.6 用戶虛擬地址空間的兩種布局
圖1.7 裝載ELF文件時創建虛擬地址空間
各種處理器架構自定義的函數arch_pick_mmap_layout負責選擇內存映射區域的布局。ARM64架構定義的函數arch_pick_mmap_layout如下:
arch/arm64/mm/mmap.c1???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設置標志位ADDRCOMPAT?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))#endifarch/arm64/mm/mmap.c**#define?MIN_GAP?(SZ_128M?+?((STACK_RND_MASK?<?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.cstatic?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所示。
圖1.8 ARM64架構的內核地址空間布局
(1)線性映射區域的范圍是[PAGE_OFFSET, 264?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區域的范圍是[VMALLOCSTART, 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)是一個動態的內存錯誤檢查工具。它為發現釋放后使用和越界訪問這兩類缺陷提供了快速和綜合的解決方案。
基于ARM64架構的Linux 4.x內核
全面介紹內核引導、進程管理、內存管理、異常處理、互斥技術和文件系統等關鍵子系統的實現。
內核引導部分詳解從處理器上電到用戶空間的進程產生的整個過程,并介紹多處理器系統的啟動過程。
結合源代碼分析,詳細解讀每種技術的使用方法及其原理。
通過圖例幫助讀者理解各種數據結構之間的關系。
通過執行流程圖幫助讀者理解函數的執行過程。
對同類技術進行歸納總結和對比分析,例如3種塊分配器、巨型頁的兩種實現、解決內存碎片問題的各種技術、3種中斷下半部和3種RCU技術,等等。
本文轉載自異步社區。
原文鏈接:https://www.epubit.com/articleDetails?id=a8b0a5c19afc4cb797bf8c0a3e2a7846
任務調度 虛擬化
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。