內(nèi)存管理知識點(diǎn)(持續(xù)補(bǔ)充...)

虛擬內(nèi)存

Android 現(xiàn)在都是采用ARM64的架構(gòu)了,采用64位的cpu總線,雖然說不至于全部64位地址都使用,不過也不像32位那樣緊緊巴巴過日子了。不過很多l(xiāng)inux的介紹文章,還是只介紹了32位地址空間的內(nèi)存管理,虛擬內(nèi)存還是3:1或者2:2(32位地址空間最大內(nèi)存是4GB),這就太out了。

64位地址空間中,
[0xFFFF_0000_0000_0000,0xFFFF_FFFF_FFFF_FFFF] 是內(nèi)核態(tài)地址
....
中間部分都是不規(guī)范的,如今使用中間的地址,手機(jī)是會(huì)掛掉的
....
[0x0000_0000_0000_0000,0x0000_FFFF_FFFF_FFFF] 是用戶態(tài)地址

**CONFIG_COMPAT = y **打開的話,說明還支持32位進(jìn)程

**CONFIG_ARM64_VA_BITS = 39 **
因?yàn)?8位地址的總內(nèi)存可以達(dá)到256TB,太夸張了,所以一般都是打開這個(gè)配置,使用39位地址,512GB就行了。內(nèi)核態(tài)空間和用戶態(tài)空間都是512G
內(nèi)核起始地址就是最高位減去 2的39次方 +1

#define VA_BITS         (CONFIG_ARM64_VA_BITS)
#define VA_START        (UL(0xffffffffffffffff) - (UL(1) << VA_BITS) + 1)

用戶態(tài)空間中的排列一般都是固定的,這個(gè)就是分段,然后再把這些放在不連續(xù)的內(nèi)存塊中,把內(nèi)存塊分頁存儲。

  • 頂部是棧(stack),存放局部變量和實(shí)現(xiàn)函數(shù)調(diào)用,自上而下增長。
  • 棧和堆中間是文件區(qū)間映射到虛擬地址空間的內(nèi)存映射區(qū)域。
  • 堆(heap),動(dòng)態(tài)分配和釋放的內(nèi)存,自下而上增長。
  • BSS,未初始化的數(shù)據(jù)段,存放進(jìn)程未初始化的static 以及 gloal 變量, 默認(rèn)初始化時(shí)全部為0.
  • Data,數(shù)據(jù)段
  • Text(ELF)代碼段

用戶程序使用了一段內(nèi)存,首先會(huì)在虛擬內(nèi)存上面找到一段空的內(nèi)存,然后將用戶程序使用的內(nèi)存映射到這段內(nèi)存上,然后虛擬內(nèi)存再將這段內(nèi)存映射到物理內(nèi)存上。
第一次映射需要段表,第二次映射需要頁表。
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)中分配物理頁,建立頁表中把虛擬地址映射到物理地址。

問題一:頁表越來越大,內(nèi)存浪費(fèi)
方案:多級頁表,建立頁表索引,不使用的頁表不加載到內(nèi)存中

問題二:多級頁表級數(shù)過多,造成訪問次數(shù)增多,執(zhí)行指令的速度比訪問速度快得多
方案:CPU和內(nèi)存間加個(gè)快表,TSL

一個(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)存。

Pagefault,是CPU提供的功能。兩種情況會(huì)出現(xiàn)Pagefault,一是,CPU通過虛擬地址沒有查到對應(yīng)的物理地址。二是,MMU沒有訪問物理地址的權(quán)限。

分DMA Zone的原因,是DMA引擎的缺陷。DMA引擎 可以直接訪問內(nèi)存空間的地址,但不一定能夠訪問到所有的內(nèi)存,訪問內(nèi)存時(shí)會(huì)存在一定的限制。

當(dāng)CPU 和DMA同時(shí)訪問內(nèi)存時(shí),硬件上會(huì)有仲裁器,選擇優(yōu)先級高的去訪問內(nèi)存。

內(nèi)核初始化后會(huì)將物理內(nèi)存線性映射,這樣通過物理地址和虛擬地址的偏移就可以獲得頁表物理地址對應(yīng)的虛擬地址

分配內(nèi)存的系統(tǒng)調(diào)用

在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)存。

  • 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)行匿名映射和文件映射,文件映射即把磁盤存儲設(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)存分配。

這兩種方式分配的都是虛擬內(nèi)存,沒有分配物理內(nèi)存。在第一次訪問已分配的虛擬地址空間的時(shí)候,發(fā)生缺頁中斷,操作系統(tǒng)負(fù)責(zé)分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。虛擬地址的分配也是內(nèi)核態(tài)管理的,不過用戶進(jìn)程可以訪問用戶態(tài)的地址空間。

分配器

如果進(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,安卓R上AOSP默認(rèn)采用scudo,不過性能會(huì)有跌落,都切換回jemalloc了。

舉例

某64位進(jìn)程A,在用戶態(tài)堆區(qū)malloc段堆內(nèi)存,首先是調(diào)用glib.c,嘗試從jemalloc(管理虛擬內(nèi)存,防止碎片化,分配判定,小塊內(nèi)存分配)中分配,如果有,就分配到該區(qū)域,不過還沒有物理內(nèi)存,只有訪問這塊虛擬內(nèi)存的時(shí)候,會(huì)產(chǎn)生pagefault異常,進(jìn)入內(nèi)核態(tài),內(nèi)核態(tài)再去調(diào)用alloc_pages或者get_free_page分配物理頁面,再通過頁表映射。完成映射后,會(huì)再次訪問這塊虛擬內(nèi)存,這個(gè)時(shí)候沒有異常產(chǎn)生,可以訪問了!進(jìn)程只能訪問已經(jīng)分配的虛擬地址空間,分配還是依賴操作系統(tǒng)取分配,哪怕是虛擬地址空間!

32位進(jìn)程在ARM64上

0-3G是虛擬地址,3-4G是內(nèi)核地址,894M線性映射,仍然存在高端內(nèi)存去映射其他物理地址。虛擬地址空間到物理空間的轉(zhuǎn)換建立在頁表上,不同的用戶頁表不一樣。內(nèi)核部分頁表相同,所有程序都能訪問內(nèi)核空間。

DDR

Dual Data Rate SDRAM 雙倍速率同步同態(tài)隨機(jī)存儲器

DDR的初始化一般在BIOS或者BootLoader中完成,BIOS或者BootLoader把DDR的大小傳遞給Linux內(nèi)核,因此從Linux內(nèi)核角度看,DDR就是一段物理空間內(nèi)存

32位應(yīng)用在ARM64上

ARM公司宣稱64位的ARMv8是兼容32位的ARM應(yīng)用的,所有的32位應(yīng)用都可以不經(jīng)修改就在ARMv8上運(yùn)行。那32位應(yīng)用的虛擬地址在64位內(nèi)核上是怎么分布的呢?事實(shí)上,64位內(nèi)核上的所有進(jìn)程都是一個(gè)64位進(jìn)程。要運(yùn)行32位的應(yīng)用程序, Linux內(nèi)核仍然從64位init進(jìn)程創(chuàng)建一個(gè)進(jìn)程, 但將用戶地址空間限制為4GB。通過這種方式, 我們可以讓64位Linux內(nèi)核同時(shí)支持32位和64位應(yīng)用程序。

要注意的是, 32位應(yīng)用程序仍然對應(yīng)128TB的內(nèi)核虛擬地址空間, 并且不與內(nèi)核共享自己的4GB虛擬地址空間, 此時(shí)用戶應(yīng)用程序具有完整的4GB虛擬地址。而32位內(nèi)核上的32位應(yīng)用程序只有3GB真正意義上的虛擬地址空間。

virt_to_phys宏的作用是將內(nèi)核虛擬地址轉(zhuǎn)換成物理地址(針對線性映射區(qū)域)

頁表遍歷過程

頁表遍歷過程
下面以arm64處理器架構(gòu)多級頁表遍歷作為結(jié)束(使用4級頁表,頁大小為4K):

Linux內(nèi)核中 可以將頁表擴(kuò)展到5級,分別是頁全局目錄(Page Global Directory, PGD), 頁4級目錄(Page 4th Directory, P4D), 頁上級目錄(Page Upper Directory, PUD),頁中間目錄(Page Middle Directory, PMD),直接頁表(Page Table, PT),而支持arm64的linux使用4級頁表結(jié)構(gòu)分別是 pgd, pud, pmd, pt ,arm64手冊中將他們分別叫做L0,L1,L2,L3級轉(zhuǎn)換表,所以一下使用L0-L3表示各級頁表。

tlb miss時(shí),mmu會(huì)進(jìn)行多級頁表遍歷遍歷過程如下:

1.mmu根據(jù)虛擬地址的最高位判斷使用哪個(gè)頁表基地址寄存器作為起點(diǎn):當(dāng)最高位為0時(shí),使用ttbr0_el1作為起點(diǎn)(訪問的是用戶空間地址);當(dāng)最高位為1時(shí),使用ttbr1_el1作為起點(diǎn)(訪問的是內(nèi)核空間地址) mmu從相應(yīng)的頁表基地址寄存器中獲得L0轉(zhuǎn)換表基地址。

2.找到L0級轉(zhuǎn)換表,然后從虛擬地址中獲得L0索引,通過L0索引找到相應(yīng)的表項(xiàng)(arm64中稱為L0表描述符,內(nèi)核中叫做PGD表項(xiàng)),從表項(xiàng)中獲得L1轉(zhuǎn)換表基地址。

3.找到L1級轉(zhuǎn)換表,然后從虛擬地址中獲得L1索引,通過L1索引找到相應(yīng)的表項(xiàng)(arm64中稱為L1表描述符,內(nèi)核中叫做PUD表項(xiàng)),從表項(xiàng)中獲得L2轉(zhuǎn)換表基地址。

4.找到L2級轉(zhuǎn)換表,然后從虛擬地址中獲得L2索引,通過L2索引找到相應(yīng)的表項(xiàng)(arm64中稱為L2表描述符,內(nèi)核中叫做PUD表項(xiàng)),從表項(xiàng)中獲得L3轉(zhuǎn)換表基地址。

5.找到L3級轉(zhuǎn)換表,然后從虛擬地址中獲得L3索引,通過L3索引找到頁表項(xiàng)(arm64中稱為頁描述符,內(nèi)核中叫做頁表項(xiàng))。

6.從頁表項(xiàng)中取出物理頁幀號然后加上物理地址偏移(VA[11,0])獲得最終的物理地址。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容