隨著cpu技術(shù)發(fā)展,現(xiàn)在大部分移動(dòng)設(shè)備、PC、服務(wù)器都已經(jīng)使用上64bit的CPU,但是關(guān)于Linux內(nèi)核的虛擬內(nèi)存管理,還停留在歷史的用戶態(tài)與內(nèi)核態(tài)虛擬內(nèi)存3:1的觀念中,導(dǎo)致在解決一些內(nèi)存問題時(shí)存在誤解。
例如現(xiàn)在主流的移動(dòng)設(shè)備操作系統(tǒng)Android,經(jīng)常遇到進(jìn)程使用大量內(nèi)存導(dǎo)致被lmk殺死,分配不到內(nèi)存而觸發(fā)OOM/ANR,或者分配內(nèi)存慢導(dǎo)致卡頓,內(nèi)核態(tài)使用哪個(gè)分配內(nèi)存的函數(shù)更合理等問題,有些涉及物理內(nèi)存分配,有些涉及虛擬內(nèi)存分配,如果不熟悉虛擬內(nèi)存管理的技術(shù)知識(shí),可能走很多彎路。
我們計(jì)劃通過一系列文章來介紹虛擬內(nèi)存分配/釋放,缺頁處理,內(nèi)存壓縮/回收,內(nèi)存分配器等知識(shí),梳理虛擬內(nèi)存的管理。本章節(jié)結(jié)合代碼介紹進(jìn)程虛擬內(nèi)存布局以及進(jìn)程的虛擬內(nèi)存分配釋放流程,涉及的代碼是android-8.1, 內(nèi)核版本kernel-4.9,架構(gòu)是arm64。
進(jìn)程虛擬內(nèi)存空間
虛擬地址空間分布
理論上,64bit地址支持訪問的地址空間是[0, 2(64-1)],而實(shí)際上現(xiàn)有的應(yīng)用程序都不會(huì)用這么大的地址空間,并且arm64芯片現(xiàn)在也不支持訪問這么大的地址空間,arm64架構(gòu)芯片最大支持訪問48bit的地址空間。例如在Android系統(tǒng)中,整個(gè)虛擬地址空間分成兩部分,如下圖所示:
其中[0x0001000000000000,0xFFFF000000000000]之間的地址是不規(guī)范地址,不能使用;該段內(nèi)存把整個(gè)虛擬地址空間劃分為兩段,低段內(nèi)存為進(jìn)程用戶態(tài)地址空間,高段內(nèi)存為內(nèi)核地址空間。參考代碼(archarm64includeasmmemory.h):
如果內(nèi)核打開CONFIG_COMPAT選項(xiàng),說明用戶態(tài)既支持64位進(jìn)程,也支持32位進(jìn)程;由于32bit的地址最多可以訪問的虛擬地址空間最多只有4GB(232 Byte),所以32位進(jìn)程的用戶態(tài)進(jìn)程地址空間與64位進(jìn)程是有區(qū)別的。
32位進(jìn)程的用戶態(tài)地址空間是[0x0, 0x00000000FFFF_FFFF]
64位進(jìn)程的用戶態(tài)地址空間是[0x0, 0x0000FFFFFFFF_FFFF]
從代碼看出,32bit進(jìn)程用戶空間大小是4GB,64bit進(jìn)程的虛擬內(nèi)存大小與CONFIG_ARM64_VA_BITS的值相關(guān);如果CONFIG_ARM64_VA_BITS是48bit則可以達(dá)到256TB,現(xiàn)在的移動(dòng)設(shè)備顯然用不到這么大的內(nèi)存空間,所以大部分Android設(shè)備中CONFIG_ARM64_VA_BITS默認(rèn)配置的是39,即64bit進(jìn)程的最大虛擬地址空間大小是512GB。
雖然32bit或者64bit的進(jìn)程在用戶態(tài)內(nèi)存空間大小不一樣,但是當(dāng)它們陷入到內(nèi)核態(tài)后,訪問的內(nèi)核空間地址是沒有差異的,都是從VA_START開始,直到0xFFFFFFFFFFFFFFFF結(jié)束,也是512GB。
每個(gè)進(jìn)程的虛擬地址空間主要分為如下幾個(gè)區(qū)域(如圖):
代碼段(text)、數(shù)據(jù)段(data)和未初始化數(shù)據(jù)段(bss)。
動(dòng)態(tài)庫的代碼段、數(shù)據(jù)段和未初始化數(shù)據(jù)段。
堆(heap),動(dòng)態(tài)分配和釋放的內(nèi)存。
棧(stack),存放局部變量和實(shí)現(xiàn)函數(shù)調(diào)用。
環(huán)境變量和參數(shù)字符串的存儲(chǔ)區(qū)。
文件區(qū)間映射到虛擬地址空間的內(nèi)存映射區(qū)域。
其中Data Segment、BSS segment、Heap段統(tǒng)稱為數(shù)據(jù)點(diǎn)。
幾種地址的概念
介紹完虛擬內(nèi)存地址空間,澄清幾種地址的概念:物理地址、線性地址、邏輯地址三種地址的含義。
物理地址
每片物理內(nèi)存存儲(chǔ)實(shí)際地址,例如一個(gè)8GB的內(nèi)存,0x00000000表示第一個(gè)byte的地址,而0xFFFFFFFF表示的是最后一個(gè)byte的地址;物理地址的值與實(shí)際的內(nèi)存條上的地址一一對應(yīng),物理地址的大小與cpu訪問物理內(nèi)存的總線寬度有一定的關(guān)系。
線性地址
為了保證系統(tǒng)多任務(wù)運(yùn)行的安全性和可靠性(防止一個(gè)任務(wù)篡改系統(tǒng)或者其他任務(wù)的內(nèi)存),CPU增加段頁式內(nèi)存管理;段基地址+段內(nèi)偏移構(gòu)成的地址就是線性地址;如果開啟的分頁內(nèi)存管理,線性地址還要通過MMU計(jì)算才能轉(zhuǎn)換出物理地址。
邏輯地址
每個(gè)進(jìn)程運(yùn)行時(shí)CPU看到的地址就是邏輯地址,實(shí)際上也是線性地址中的段內(nèi)偏移地址,邏輯地址與段基地址可以計(jì)算出線性地址。
進(jìn)程在訪問虛擬地址空間的任意合法地址時(shí),都要按照邏輯地址->線性地址->物理地址的順序換算才能找到對應(yīng)的物理地址;由于段式內(nèi)存管理存在性能、訪問效率的問題,以及Linux要兼容各種CPU,在Linux內(nèi)核中所有的用戶態(tài)進(jìn)程使用的同一個(gè)段,且段基地址都是0,如此既可以兼容的傳統(tǒng)的段式內(nèi)存管理,又可以通過頁式內(nèi)存映射更靈活的管理內(nèi)存。由于同一個(gè)段基地址都是0,對每個(gè)進(jìn)程來說,邏輯地址和線性地址是一樣的;同時(shí)每個(gè)進(jìn)程的PGD是不一樣的,從而保證每個(gè)進(jìn)程之間隔離,不同進(jìn)程同一個(gè)虛擬地址映射的物理地址就不一樣了。
Linux系統(tǒng)采用延遲分配物理內(nèi)存的策略,用戶態(tài)進(jìn)程每次分配內(nèi)存時(shí)分配的都是虛擬內(nèi)存,表示一段地址空間已經(jīng)分配出來供進(jìn)程使用;當(dāng)進(jìn)程第一次訪問虛擬地址時(shí),才會(huì)發(fā)現(xiàn)虛擬地址沒有對應(yīng)的物理內(nèi)存,系統(tǒng)默認(rèn)會(huì)觸發(fā)缺頁異常,從內(nèi)核物理內(nèi)存管理系統(tǒng)中分配物理頁,建立頁表中把虛擬地址映射到物理地址。對于缺頁異常處理流程,頁表創(chuàng)建/建立/銷毀等操作在以后文章中介紹。
分配內(nèi)存的系統(tǒng)調(diào)用
在Linux系統(tǒng)中,虛擬內(nèi)存和物理內(nèi)存都是由kernel管理的,當(dāng)進(jìn)程需要分配內(nèi)存時(shí),都需要通過系統(tǒng)調(diào)用陷入到內(nèi)核空間分配,再虛擬內(nèi)存起始地址返回到用戶態(tài);內(nèi)核提供了多個(gè)系統(tǒng)調(diào)用來分配虛擬內(nèi)存,包括brk、mmap和mremap等。
brk系統(tǒng)調(diào)用
brk是傳統(tǒng)分配/釋放堆內(nèi)存的系統(tǒng)調(diào)用, 堆內(nèi)存是由低地址向高地址方向增長;
分配內(nèi)存時(shí),將數(shù)據(jù)段(.data)的最高地址指針_edata往高地址擴(kuò)展;
釋放內(nèi)存時(shí),把_edata向低地址收縮。
可以看出brk系統(tǒng)調(diào)用管理的始終是一片連續(xù)的虛擬地址空間,而且起始地址一經(jīng)設(shè)定就默認(rèn)不變,只是高地址按需變化。
mmap系統(tǒng)調(diào)用
mmap系統(tǒng)調(diào)用是在進(jìn)程堆和棧中間(稱為Memory Mapping Segment)找一塊空閑的虛擬內(nèi)存,mmap可以進(jìn)行匿名映射和文件映射,文件映射即把磁盤存儲(chǔ)設(shè)備上面的文件映射的內(nèi)存中,然后訪問內(nèi)存就是訪問文件,文件映射的物理頁是可以通過kswapd或者direct reclaim回收的;匿名映射即沒有映射任何文件。
由于brk系統(tǒng)調(diào)用分配內(nèi)存存在內(nèi)存碎片化線性,例如先分配100MB的內(nèi)存,然后再分配4KB內(nèi)存,再把100MB內(nèi)存釋放掉,此時(shí)由于4KB內(nèi)存還沒有釋放,_edata就不能收縮,導(dǎo)致100MB內(nèi)存不能及時(shí)操作系統(tǒng);反之先分配4KB,在分配100MB,則存在內(nèi)存碎片化的問題。另外由于_edata上面是mmap區(qū)域,_edata與最近的mmap內(nèi)存很接近,則會(huì)導(dǎo)致brk系統(tǒng)調(diào)用極容易分配失敗,即使memory mmap區(qū)域還有大量可用內(nèi)存。Brk分配管理的實(shí)際上就是一塊匿名映射的內(nèi)存,所以實(shí)際上可以通過mmap匿名映射來滿足malloc的內(nèi)存分配。在Linux操作系統(tǒng)標(biāo)準(zhǔn)libc庫中,malloc函數(shù)的實(shí)現(xiàn)中會(huì)根據(jù)分配內(nèi)存的size來決定使用哪個(gè)分配函數(shù), 當(dāng)size小于等于128KB,調(diào)用brk分配, 當(dāng)size大于128KB時(shí),調(diào)用mmap分配內(nèi)存。
這兩種方式分配的都是虛擬內(nèi)存,沒有分配物理內(nèi)存。在第一次訪問已分配的虛擬地址空間的時(shí)候,發(fā)生缺頁中斷,操作系統(tǒng)負(fù)責(zé)分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。
分配器
如果進(jìn)程每次分配內(nèi)存都通過brk和mmap系統(tǒng)調(diào)用分配的話,存在兩個(gè)致命的問題:
碎片化的問題,從內(nèi)核分配虛擬內(nèi)存都是按照page(默認(rèn)是4KB)對齊來分配的,如果進(jìn)程分配8byte,實(shí)際從內(nèi)核分配的內(nèi)存是4096byte,這樣就存在4088byte的浪費(fèi);同時(shí)進(jìn)程的內(nèi)存分配需求存在隨機(jī)性,如果不同大小的內(nèi)存交替分配,當(dāng)部分內(nèi)存釋放后,整個(gè)內(nèi)存空間嚴(yán)重碎片化,導(dǎo)致最后分配大片內(nèi)存時(shí)高概率會(huì)失敗。
性能問題,系統(tǒng)調(diào)用從用戶態(tài)陷入到內(nèi)核態(tài)都是通過中斷來實(shí)現(xiàn)的,在進(jìn)程從內(nèi)核態(tài)返回到用戶態(tài)時(shí),任務(wù)有可能被調(diào)度出cpu;另外,對于多線程的進(jìn)程,所有的線程共享同一個(gè)mm,如果多個(gè)線程同時(shí)分配內(nèi)存,則在內(nèi)核空間存在競爭關(guān)系,所有的線程分配請求都要排隊(duì)處理;如果頻繁系統(tǒng)調(diào)用分配內(nèi)存,分配內(nèi)存的效率會(huì)降低。
分配器的出現(xiàn)就是為了解決上述問題,例如我們熟悉的libc庫,調(diào)用malloc的時(shí)候并不是每次都會(huì)通過系統(tǒng)調(diào)用從內(nèi)核分配內(nèi)存的,而是分配器相當(dāng)于在malloc和系統(tǒng)調(diào)用之間插入一層中間件。分配器首先通過系統(tǒng)調(diào)用從內(nèi)核批發(fā)大塊內(nèi)存,然后切成不同大小的內(nèi)存片緩存起來,例如8/16/24/32/64byte等,當(dāng)調(diào)用malloc的時(shí)候,直接從cache的空閑小內(nèi)存片分配;同時(shí)為了解決性能問題,分配器對每個(gè)線程或者每個(gè)cpu預(yù)留單獨(dú)的cache,每個(gè)線程從自己的cache中分配,可以減少線程之間的鎖競爭。
現(xiàn)在業(yè)界主流的分配器有ptmalloc、tcmalloc、jemalloc、scudo等。在Android系統(tǒng)中,為例提高兼容性和性能,malloc函數(shù)的實(shí)現(xiàn),默認(rèn)都是通過mmap系統(tǒng)調(diào)用分配內(nèi)存,不再使用brk系統(tǒng)調(diào)用(部分三方APP自帶SDK可能會(huì)用brk)。Android現(xiàn)在用的分配器是jemalloc或者scudo,關(guān)于分配啟動(dòng)實(shí)現(xiàn)本文不再贅述。
進(jìn)程分配內(nèi)存核心函數(shù)
本節(jié)介紹brk、mmap、munmap函數(shù)的實(shí)現(xiàn)所用到的幾個(gè)核心函數(shù)。
幾個(gè)關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)
在介紹進(jìn)程如何分配到虛擬內(nèi)存之前,先了解幾個(gè)進(jìn)程內(nèi)存管理相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
struct mm_struct
每個(gè)進(jìn)程或內(nèi)核線程都由一個(gè)任務(wù)描述數(shù)據(jù)結(jié)構(gòu)(task_struct)來管理,每個(gè)task_struct中有個(gè)struct mm_strcut數(shù)據(jù)結(jié)構(gòu)指針,用來管理任務(wù)的虛擬地址空間;而內(nèi)核線程是沒有用戶態(tài)虛擬地址空間,所以其mm字段為NULL;mm的數(shù)據(jù)結(jié)構(gòu)如下:
struct mm_struct是每個(gè)task的虛擬內(nèi)存空間的描述符,例如用戶態(tài)進(jìn)(線)程棧區(qū)間,堆區(qū)間的地址和大小等;每個(gè)進(jìn)程只有一個(gè)mm,即使是多個(gè)線程的進(jìn)程,所有的線程都是共享同一個(gè)mm,mm_struct數(shù)據(jù)結(jié)構(gòu)中幾個(gè)關(guān)鍵字段的含義如下:
struct vm_area_struct
分配的每個(gè)虛擬內(nèi)存區(qū)域都由一個(gè)vm_area_struct 數(shù)據(jù)結(jié)構(gòu)來管理,包括虛擬內(nèi)存的起始和結(jié)束地址,以及內(nèi)存的訪問權(quán)限等,通常命名為vma;vm_area_struct 數(shù)據(jù)結(jié)構(gòu)的定義如下:
mm_struct和vm_area_struct描述的都是進(jìn)程的虛擬地址空間,所謂的“虛擬”,意思是指進(jìn)程有相應(yīng)大小內(nèi)存需求,一個(gè)虛擬內(nèi)存地址區(qū)域表示該段內(nèi)存已經(jīng)分配出去,但是并不保證該地址空間已經(jīng)映射物理內(nèi)存,也不保證相應(yīng)的物理頁在內(nèi)存中。例如分配2MB的內(nèi)存后,自始至終沒有訪問過這片內(nèi)存,所以這2MB的內(nèi)存只是占用了虛擬地址空間,沒有使用相應(yīng)大小的物理內(nèi)存。
當(dāng)訪問一個(gè)未經(jīng)映射的虛擬地址時(shí),就會(huì)產(chǎn)生一個(gè)“Page Fault”事件(通常叫做缺頁異常),當(dāng)前進(jìn)程會(huì)被缺頁異常打斷而進(jìn)入異常處理函數(shù),在處理函數(shù)中,會(huì)從伙伴系統(tǒng)中分配一個(gè)page,與相應(yīng)的虛擬地址建立映射,這個(gè)映射關(guān)系需要通過頁表來管理;同時(shí)頁表也需要單獨(dú)分配內(nèi)存來保存,所以在計(jì)算一個(gè)進(jìn)程使用的物理內(nèi)存時(shí),也要算上頁表的內(nèi)存。
在一個(gè)mm中,所有的vma通過兩種結(jié)構(gòu)管理一起來,一個(gè)是雙向鏈表,一個(gè)是紅黑樹。當(dāng)遍歷這個(gè)虛擬地址空間時(shí),通過雙向鏈表是常用的方法;當(dāng)在虛擬地址空間查找vma是,通過紅黑樹查找是更便捷的方法。通常兩種方法會(huì)結(jié)合起來使用,例如通過紅黑樹查找到某個(gè)vma,后要找到該vma的前置,則直接通過vma->vm_prev就可以直接獲取。通過一個(gè)圖表展示一下幾個(gè)數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系:
幾個(gè)關(guān)鍵的函數(shù)
arch_pick_mmap_layout
進(jìn)程虛擬內(nèi)存映射存在兩種布局方式,主要區(qū)別是mmap_base值和分配虛擬內(nèi)存增長方向。
傳統(tǒng)布局
映射區(qū)域自底向上增長,mmap_base的值是TASK_UNMAPPED_BASE,ARM64架構(gòu)中定義為TASK_SIZE/4。內(nèi)核默認(rèn)啟用內(nèi)存映射區(qū)域隨機(jī)化,在該起始地址加上一個(gè)隨機(jī)值。傳統(tǒng)布局的缺點(diǎn)是堆的最大長度受到限制,例如_edata的值增長會(huì)受到mmap_base的限制,在32位系統(tǒng)中影響比較大,在64位系統(tǒng)中則不是緊急的問題。
新布局
內(nèi)存映射區(qū)域自頂向下增長,mmap_base的值是(STACK_TOP – STACK_GAP)。默認(rèn)啟用內(nèi)存映射區(qū)域隨機(jī)化,需要把起始地址再減去一個(gè)隨機(jī)值。
兩種布局如下圖所示:
開啟地址隨機(jī)化
在進(jìn)程調(diào)用execve以裝載ELF文件的時(shí),load_elf_binary會(huì)創(chuàng)建進(jìn)程的用戶虛擬地址空間。如果進(jìn)程描述符的成員personality沒有設(shè)置標(biāo)志位ADDR_NO_RANDOMIZE(該標(biāo)志位表示禁止虛擬地址空間隨機(jī)化),并且全局變量randomize_va_space是非零值,那么給進(jìn)程設(shè)置標(biāo)志PF_RANDOMIZE,允許虛擬地址空間隨機(jī)化。
不同CPU架構(gòu)內(nèi)存映射區(qū)域的布局可能不一樣,所以不同arch都要實(shí)現(xiàn)自己的arch_pick_mmap_layout函數(shù)。ARM64架構(gòu)定義的函數(shù)arch_pick_mmap_layout如下:
如果開啟了地址隨機(jī)化,則通過arch_mmap_rnd計(jì)算獲取一個(gè)隨機(jī)值;計(jì)算隨機(jī)值是有范圍的:
在傳統(tǒng)布局中,隨機(jī)范圍是[0, ((1UL << mmap_rnd_compat_bits) - 1)<
在新布局中,隨機(jī)值范圍[0,((1UL << mmap_rnd_bits) - 1)<
arch_get_unmapped_area 和 arch_get_unmapped_area_topdown函數(shù)都用到一個(gè)核心數(shù)據(jù)結(jié)構(gòu)struct vm_unmapped_area_info,這個(gè)數(shù)據(jù)結(jié)構(gòu)用于管理分配內(nèi)存請求。
傳統(tǒng)布局,查找空閑內(nèi)存的范圍是[mm->mmapbase, TASKSIZE],實(shí)現(xiàn)該功能的函數(shù)arch_get_unmapped_area代碼如下:
1.如果是文件映射分配內(nèi)存,filp指向?qū)?yīng)打開的文件描述數(shù)據(jù)結(jié)構(gòu),如果是匿名映射,filp為NULL。addr是建議分配內(nèi)存起始地址,如果以addr開始的地址恰好是空閑的,且滿足本次分配需求則返回成功,參考15~22行代碼;如果不滿足需求,則初始化info,調(diào)用vm_unmapped_area函數(shù)來掃描mmap映射區(qū)域來查找滿足請求的內(nèi)存。
2.len表示本次請求分配內(nèi)存的長度。pgoff表示分配的內(nèi)存映射到filp描述的文件中的偏移,如果是匿名映射,該參數(shù)是忽略的。flags表示本次分配內(nèi)存的屬性和權(quán)限信息。
新布局中,遍歷內(nèi)存的方法稍微不同,先看下代碼:
1.參數(shù)的含義與arch_get_unmapped_area相同
2.新的布局與傳統(tǒng)布局分配新內(nèi)存的行為有差異,當(dāng)從高到低的方向分配內(nèi)存失敗的情況下,會(huì)再次從低到高的方向分配一次。28~32行代碼,設(shè)置flag為VM_UNMAPPED_AREA_TOPDOWN,并從mm->mmap_base到max(PAGESIZE, mmap_min_addr)從高地址向低地址分配一次,用offset_in_page判斷分配是否成功,由于在分配成功的情況下,分配的addr是page對齊的,所以addr的低12bit都是0,而如果addr的低12bit的值不是0,則說明分配失敗。
從41~46行代碼看出,flag已經(jīng)設(shè)置為0(方向變成由低到高),同時(shí)遍歷的區(qū)間變成了[TASK_UNMAPPED_BASE, TASK_SIZE]。
unmapped_area
從vm_unmapped_area函數(shù)看出,unmapped_area實(shí)現(xiàn)由低到高的方向分配內(nèi)存的方法,unmapped_area_topdown實(shí)現(xiàn)由高到低的方向分配內(nèi)存的方法。
回顧一下,進(jìn)程虛擬地址空間中所有vma按照地址從小到大的順序,分別記錄在一個(gè)雙向鏈表和一個(gè)紅黑樹里面,通過鏈表可以快速遍歷所有分配的內(nèi)存信息,例如proc/$pid/maps和/proc/$pid/smaps兩個(gè)節(jié)點(diǎn)的實(shí)現(xiàn);通過遍歷紅黑樹可以快速查找到包含指定地址的vma,例如分配內(nèi)存時(shí)查找到空閑內(nèi)存。
在vma的紅黑樹中,每個(gè)節(jié)點(diǎn)的左子樹上所有內(nèi)存地址都小于其右子樹上的所有內(nèi)存地址,傳統(tǒng)布局中采用中序遍歷的方式從根開始遍歷所有vma查找空閑內(nèi)存,先從左子樹開始遍歷,直到找到最左邊的滿足分配需求的內(nèi)存;如果在根的左子樹上面沒有找到,則開始遍歷右子樹,以右子樹為根遞歸遍歷;為了提高效率,每個(gè)vma的rb_subtree_gap值表示該樹最大的空閑內(nèi)存大小,如果連根節(jié)點(diǎn)的rb_subtree_gap都不滿足分配需求,則說進(jìn)程已經(jīng)OOM;如果滿足需求,則開始遍歷找到滿足請求的空間并返回起始地址。
16~27行代碼首先對入?yún)⑦M(jìn)行合法性判斷,其中16行代碼info->length + info->align_mask在length的基礎(chǔ)上加上對齊的mask,防止執(zhí)行到最后由于對齊的問題導(dǎo)致分配失敗,但是此處也存在缺陷:空閑內(nèi)存的長度和對齊方式恰好都滿足需求,而此處加上mask導(dǎo)致提前分配失敗,這是個(gè)極端情況,即使出現(xiàn)也說明空閑內(nèi)存已經(jīng)不充足。
30~31行代碼,當(dāng)進(jìn)程第一次進(jìn)行內(nèi)存分配時(shí),紅黑樹最開始原本就是空的,說明此時(shí)空閑內(nèi)存是充足的,所以直接跳到84行開始分配內(nèi)存。
32~34行,獲取根節(jié)點(diǎn)的vma,并判斷rb_subtree_gap是否滿足分配需求,如果不滿足需求則只能查看最后一個(gè)vma->vm_end到虛擬地址空間最大值之間的內(nèi)存是否滿足需求;由于從低向高方向分配內(nèi)存時(shí),紅黑樹最右側(cè)vma的結(jié)束地址與虛擬地址空間最大值之間的這段內(nèi)存在紅黑樹中是沒有統(tǒng)計(jì)的,所以需要判斷一下。
36~82是核心代碼,首先從紅黑樹的根得知在樹種是可以找到滿足分配需求的內(nèi)存的;從39行看出,vma->vm_rb.rb_left先從根的左子樹找起,gap_end >= low_limit說明空閑內(nèi)存是在分配請求內(nèi)存上下限之間的,那么繼續(xù)找左子樹,直到找到不滿足需求的vma,即(gap_end >= low_limit && vma->vm_rb.rb_left)條件不成立,有兩種情況,第一種,gap_end >= low_limit不成立說明現(xiàn)在vma已經(jīng)超出需求上下限范圍;vma->vm_rb.rb_left不成立說明已經(jīng)找到最左節(jié)點(diǎn)了,由于是由低到高的方向分配內(nèi)存的,所以此時(shí)左邊沒有必要找了,接著判斷當(dāng)前vma與vma->vm_prev之間的空間是否滿足需求(54~56行),如果當(dāng)前vma不滿足則開始找當(dāng)前vma的右子樹(59~67行),如果在當(dāng)前vma子樹中沒有找到滿足需求的內(nèi)存空間,則從上一層根子樹中查找。
84~89行代碼,當(dāng)在紅黑樹中沒有找到滿足需求的內(nèi)存時(shí),判斷最后一個(gè)vma到虛擬地址空間最大值之間的空閑內(nèi)存是否滿足需求,如果不滿足則說明oom了。92~101表示已經(jīng)找到滿足需求的內(nèi)存空間,其中97行堆起始地址進(jìn)行對齊處理。
unmapped_area_topdown實(shí)現(xiàn)由高向低的方向分配內(nèi)存,與unmapped_area區(qū)別是遍歷的方法變化了,先從右子樹遍歷查詢,再判斷根節(jié)點(diǎn),最后從左子樹查詢,代碼不在這里介紹。
getunmappedarea
分配虛擬內(nèi)存的時(shí)候,首先需要找到一塊空閑的滿足分配需求的內(nèi)存空間,調(diào)用的函數(shù)是get_unmapped_area,代碼如下:
1.參數(shù)共5個(gè)
struct file *file,如果是匿名映射,file為NULL;如果是文件映射,file不能為空,則表示分配的內(nèi)存即將映射file中的內(nèi)容。
unsigned long addr,表示要分配內(nèi)存的起始地址。
當(dāng)addr為0時(shí),在整個(gè)虛擬地址空間中找到滿足需求的空閑內(nèi)存,對起始地址沒有特殊要求。
unsigned long len, 要分配內(nèi)存的長度,長度單位是Byte,不足PAGE_SIZE按PAGE_SIZE處理。
unsigned long pgoff,分配的內(nèi)存,映射文件內(nèi)容在文件中的起點(diǎn)。
unsigned long flags, 指定映射對象的屬性,映射選項(xiàng)和映射頁是否可以共享,LOCKED等屬性。
2.代碼分析
第8行,arch_mmap_check是個(gè)各個(gè)架構(gòu)實(shí)現(xiàn)的mmap校驗(yàn)函數(shù),主要是對固定映射,addr有大小限制,arm64架構(gòu)定義為空。
13~14行,校驗(yàn)len大小,如果超過TASK_SIZE則明顯溢出,直接返回。
16~28行,給函數(shù)指針get_area賦值,初始值為current->mm->get_unmapped_area,當(dāng)本次分配是文件映射分配內(nèi)存,需要判斷file->f_op->get_unmapped_area是否為NULL,如果不為NULL則賦值給get_area,這么操作的原因是部分文件系統(tǒng)文件映射分配虛擬內(nèi)存時(shí)有特殊的要求或操作,例如flags、len等客制化處理等;如果是匿名映射且配置了MAP_SHARED,則賦值shmem_get_unmapped_area給get_area。
30~37行,調(diào)用get_area分配新的映射空間,然后校驗(yàn)分配的地址是否有效,其中offset_in_page函數(shù)判斷的原理是:如果分配成功,addr的值一定是PAGE_ALIGN的,如果addr低12bit不為0,則說明分配失敗。
39行,安全檢查addr,security_mmap_addr函數(shù)是Linux Security Module中函數(shù),這里不詳細(xì)介紹。
當(dāng)addr不為0時(shí),如果該地址起始的內(nèi)存恰好滿足需求,返回addr;如果flasgs配置了MAP_FIXED,則不會(huì)判斷是否滿足直接返回addr;
-
Linux
+關(guān)注
關(guān)注
87文章
11207瀏覽量
208717 -
內(nèi)存管理
+關(guān)注
關(guān)注
0文章
168瀏覽量
14115
原文標(biāo)題:進(jìn)程內(nèi)存管理初探
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論