參考wowotech關於此topic的講解:
http://www.wowotech.net/memory_management/mem_init_3.html
內存初始化代碼分析(三):創建系統內存地址映射
作者:linuxer 發佈於:2016-11-24 12:08 分類:內存管理
一、前言
經過內存初始化代碼分析(一)和內存初始化代碼分析(二)的過渡,我們終於來到了內存初始化的核心部分:paging_init。當然本文不能全部解析完該函數(那需要的篇幅太長了),我們只關注創建系統內存地址映射這部分代碼實現,也就是解析paging_init中的map_mem函數。
同樣的,我們選擇的是4.4.6的內核代碼,體系結構相關的代碼來自ARM64。
二、準備階段
在進入實際的代碼分析之前,我們首先回頭看看目前內存的狀態。偌大的物理地址空間中,系統內存佔據了一段或者幾段地址空間,這些信息被保存在了memblock模塊中的memory type類型的數組中,數組中每一個memory region描述了一段系統內存信息(base、size以及node id)。OK,系統內存就這麼多,但是也並非所有的memory type類型的數組中區域都是free的,實際上,有些已經被使用或者保留使用的內存區域需要從memory type類型的一段或者幾段地址空間中摘取下來,並定義在reserved type類型的數組中。實際上,在整個系統初始化過程中(更具體的說是內存管理模塊完成初始化之前),我們都需要暫時使用memblock這樣一個booting階段的內存管理模塊來暫時進行內存的管理(收集內存佈局信息只是它的副業),每次分配的內存都是在reserved type數組中增加一個新的item或者擴展其中一個memory region的size而已。
通過memblock模塊,我們已經收集了內存佈局的信息(memory type類型的數組),也知道了free memory資源的信息(memory type類型的數組減去reserved type類型的數組,從集合論的角度看,reserved type類型的數組是memory type類型的數組的真子集),但是,要管理這些珍貴的系統內存,首先要能夠訪問他們啊(順便說一句:memblock中的那些數組定義的地址都是物理地址),通過前面的分析文章,我們知道有兩段內存已經是可見的(完成了地址映射),一個是kernel image段,另外一個是fdt段。而廣大的系統內存區域仍然在黑暗之中,等待我們去拯救(進行地址映射)。
最後,我們思考這樣一個問題:是否memory type類型的數組代表了整個的系統內存的地址空間呢?當然不是,有些驅動可能會保留一段系統內存區域爲自己使用,同時也不希望OS管理這段內存(或者說對OS不可見),而是自己創建該段內存的地址映射。如果你對dts中的memory reserve節點比較熟悉的話,那麼實際上這樣的reserved memory region是有no-map屬性的。這時候,內核初始化過程中,在解析該reserved-memory節點的時候,會將該段地址從memblock模塊中移除。而在map_mem函數中,爲所有memory type類型的數組創建地址映射的時候,有no-map屬性的那段內存地址將不會創建地址映射,也就不在OS的控制範圍內了。
三、概覽
創建系統內存地址映射的代碼在map_mem中,如下:
static void __init map_mem(void) {
struct memblock_region *reg;
phys_addr_t limit;limit = PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE;---------------(1)
memblock_set_current_limit(limit);for_each_memblock(memory, reg) {------------------------(2)
phys_addr_t start = reg->base;――確定該region的起始地址
phys_addr_t end = start + reg->size; ――確定該region的結束地址if (start >= end)--參數檢查
break;if (ARM64_SWAPPER_USES_SECTION_MAPS) {----------------(3)
if (start < limit)
start = ALIGN(start, SECTION_SIZE);
if (end < limit) {
limit = end & SECTION_MASK;
memblock_set_current_limit(limit);
}
}
__map_memblock(start, end);-------------------------(4)
}memblock_set_current_limit(MEMBLOCK_ALLOC_ANYWHERE);----------(5)
}
(1)首先限制了當前memblock的上限。之所以這麼做是因爲在進行mapping的時候,如果任何一級的Translation table不存在的話都需要進行頁表內存的分配。而在這個時間點上,夥伴系統沒有ready,無法動態分配。當然,這時候memblock已經ready了,但是如果分配的內存都還沒有創建地址映射(整個物理內存佈局已知並且保存在了memblock模塊中的memblock模塊中,但是並非所有系統內存的地址映射都已經建立好的,而我們map_mem函數的本意就是要創建所有系統內存的mapping),內核一旦訪問memblock_alloc分配的物理內存,悲劇就會發生了。怎麼破?這裏採用了限定memblock上限的方法。一旦設定了上限,那麼memblock_alloc分配的物理內存不會高於這個上限。
設定怎樣的上限呢?基本思路就是在map_mem的調用過程中,不需要分配translation table,怎麼做到呢?當然是儘量利用已經靜態定義好的那些頁表了。PHYS_OFFSET是物理內存的起始地址,SWAPPER_INIT_MAP_SIZE 是啓動階段kernel direct mapping的size。也就是說,從PHYS_OFFSET到PHYS_OFFSET + SWAPPER_INIT_MAP_SIZE的區域,所有的頁表(各個level的translation table)都已經OK,不需要分配,只需要把描述符寫入頁表即可。因此,如果將當前memblock分配的上限設定在這裏將不會產生內存分配的動作(因爲頁表都已經ready)。
(2)對系統中所有的memory type的region建立對應的地址映射。由於reserved type的memory region是memory type的region的真子集,因此reserved memory 的地址映射也就一併建立了。
(3)如果不使用section map,那麼我們在kernel direct mapping區域靜態分配了PGD~PTE的頁表,通過起始地址對齊以及對memblock limit的設定就可以保證在create_mapping()的時候不分配頁表內存。但是在下面的情況下:
(A)Memory block的start或者end地址並沒有對齊在2M上
(B)使用section map
在這種情況下,調用create_mapping()的時候會分配pte頁表內存(沒有對齊2M,無法進行section mapping)。怎麼破?還好第一個memory block(也就是kernel image所在的block)的start address是必定對齊在2M地址上的,所以只要考慮end地址,這時候需要適當的縮小limit到end & SECTION_MASK就可以保證分配的頁表內存是已經建立地址映射的了。
(4)__map_memblock代碼如下:
static void __init __map_memblock(phys_addr_t start, phys_addr_t end)
{
create_mapping(start, __phys_to_virt(start), end - start,
PAGE_KERNEL_EXEC);
}
需要說明的是,在map_mem之後,所有之前通過__create_page_tables創建的描述符都被覆蓋了,取而代之的是新的映射,並且memory attribute如下:
#define PAGE_KERNEL_EXEC __pgprot(_PAGE_DEFAULT | PTE_UXN | PTE_DIRTY | PTE_WRITE)
大部分memory attribute保持不變(例如MT_NORMAL、PTE_AF 、 PTE_SHARED等),有幾個bit需要說明一下:PTE_UXN,Unprivileged Execute-never bit,也就是限制userspace從這裏取指執行。PTE_DIRTY是一個軟件設定的bit,硬件並不操作這個bit,OS軟件用這個bit標識該entry是clean or dirty,如果是dirty的話,說明該page的數據已經被寫入過,如果該page需要被swapped out,那麼還需要保存dirty的數據才能回收該page。關於PTE_WRITE的解釋todo。
(5)所有的系統內存的地址映射已經建立完畢,取消之前的上限,讓memblock模塊可以自由的分配內存。
四、填充PGD中的描述符
create_mapping實際上是調用底層的__create_mapping函數完成地址映射的,具體代碼如下:
static void __init create_mapping(phys_addr_t phys, unsigned long virt,
phys_addr_t size, pgprot_t prot)
{
if (virt < VMALLOC_START) {
pr_warn("BUG: not creating mapping for %pa at 0x%016lx - outside kernel range\n",
&phys, virt);
return;
}
__create_mapping(&init_mm, pgd_offset_k(virt & PAGE_MASK), phys, virt,
size, prot, early_alloc);
}
create_mapping的作用是將起始物理地址等於phys,大小是size的這一段物理內存mapping到起始虛擬地址是virt的虛擬地址空間,映射的memory attribute是prot。內核的虛擬地址空間從VMALLOC_START開始,低於這個地址就不對了,這裏需要補充一下,前面講到arm32的頁面映射時注意到映射的虛擬地址最大不能超過highmem的界限,而highmem的界限爲VMALLOC_START-8MB空間,所以這裏看到這個地方的時候就有了一個小疑惑,爲什麼arm64需要大於這個VMALLOC_START呢?
首先ARM64不存在highmem的概念,然後再看VMALLOC_START定義:
#define VMALLOC_START (VA_START)
#define VA_START (UL(0xffffffffffffffff) << VA_BITS)
#define PAGE_OFFSET (UL(0xffffffffffffffff) << (VA_BITS - 1))
#define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET + PAGE_OFFSET))
通過以上代碼可得VMALLOC_START=0xFFFFFF8000000000,而PAGE_OFFSET=0xFFFFFFC000000000
memory type的virt值對應的物理內存值除非不屬於物理內存空間範圍,否則肯定大於VMALLOC_START。
驗證完虛擬地址,底層是調用__create_mapping函數,傳遞的參數情況是這樣的,init_mm是內核空間的內存描述符,pgd_offset_k是根據給定的虛擬地址,在kernel space的pgd中找到對應的描述符的位置,early_alloc是在mapping過程中,如果需要分配內存的話(頁表需要內存),調用該函數進行內存的分配。__create_mapping函數具體代碼如下:
static void __create_mapping(struct mm_struct *mm, pgd_t *pgd,
phys_addr_t phys, unsigned long virt,
phys_addr_t size, pgprot_t prot,
void *(*alloc)(unsigned long size))
{
unsigned long addr, length, end, next;addr = virt & PAGE_MASK;------------------------(1)
length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));end = addr + length;
do {----------------------------------(2)
next = pgd_addr_end(addr, end);--------------------(3)
alloc_init_pud(mm, pgd, addr, next, phys, prot, alloc);----------(4)
phys += next - addr;
} while (pgd++, addr = next, addr != end);
}
創建地址映射熟悉要明確地址空間,不同的進程有不同的地址空間,struct mm_struct就是描述一個進程的虛擬地址空間,當然,我們這裏的場景是爲內核虛擬地址空間而創建地址映射,因此傳遞的參數是init_mm。需要創建地址映射的起始虛擬地址是virt,該虛擬地址對應的PUD中的描述符是一個8B的內存,pgd就是指向這個描述符內存的指針。
(1)因爲地址映射的最小單位是page,因此這裏進行mapping的虛擬地址需要對齊到page size,同樣的,長度也需要對齊到page size。經過對齊運算,(addr,length)定義的地址範圍應該是囊括(virt,size)定義的地址範圍,並且是對齊到page的。
(2)(addr,length)這個虛擬地址範圍可能需要佔據多個PGD entry,因此這裏我們需要一個循環,不斷的調用alloc_init_pud函數來完成(addr,length)這個虛擬地址範圍的映射,當然,alloc_init_pud函數其實也會建立下游(例如PUD、PMD、PTE)翻譯表的entry。
(3)pgd中的一個描述符只能mapping有限區域的虛擬地址(PGDIR_SIZE),pgd_addr_end的宏就是計算addr所在區域的end address。如果計算出來的end address小於傳入的end地址參數,那麼返回end參數值。也就是說,如果(addr,length)這個虛擬地址範圍的mapping需要跨越多個pgd entry,那麼next變量保存了下一個pgd entry的起始虛擬地址。
(4)這個函數有兩個作用,一是填充pgd entry,二是創建後續的pud translation table(如果需要的話)並進行下游Translation table的建立。
五、分配PUD頁表內存並填充相應的描述符
alloc_init_pud並非只是操作pud,實際上它是操作pgd的一個entry,並分配初始pud以及後續translation table的。填充PGD的entry需要給出對應PUD translation table的內存地址,如果PUD不存在,那麼alloc_init_pud還需要分配PUD translation table(page size),只有得到PUD翻譯表的物理內存地址,我們才能填充PGD entry。具體代碼如下:
static void alloc_init_pud(struct mm_struct *mm, pgd_t *pgd,
unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
void *(*alloc)(unsigned long size))
{
pud_t *pud;
unsigned long next;if (pgd_none(*pgd)) {--------------------------(1)
pud = alloc(PTRS_PER_PUD * sizeof(pud_t));
pgd_populate(mm, pgd, pud);
}
pud = pud_offset(pgd, addr); ---------------------(2)
do { --------------------------------(3)
next = pud_addr_end(addr, end);if (use_1G_block(addr, next, phys)) { ----------------(4)
pud_t old_pud = *pud;
set_pud(pud, __pud(phys | pgprot_val(mk_sect_prot(prot)))); -----(5)if (!pud_none(old_pud)) { ---------------------(6)
flush_tlb_all(); ------------------------(7)
if (pud_table(old_pud)) {
phys_addr_t table = __pa(pmd_offset(&old_pud, 0));
if (!WARN_ON_ONCE(slab_is_available()))
memblock_free(table, PAGE_SIZE); ------------(8)
}
}
} else {
alloc_init_pmd(mm, pud, addr, next, phys, prot, alloc);
}
phys += next - addr;
} while (pud++, addr = next, addr != end);
}
(1)如果當前pgd entry是全0的話,說明還沒有對應的下級PUD頁表內存,因此需要進行PUD頁表內存的分配。需要說明的是這時候,夥伴系統沒有ready,分配內存仍然使用memblock模塊,pgd_populate用來建立pgd entry和PUD 頁表內存的關係。
(2)至此,pud的頁表內存已經有了,但是addr對應PUD中的哪一個描述符呢?pud_offset給出了答案,其返回的指針指向傳入參數addr地址對應的pud 描述符內存,而我們隨後的任務就是填充pud entry了。
(3)雖然(addr,end)之間的虛擬地址範圍共享一個pgd entry,但是這個地址範圍對應的pud entry可能有多個,通過循環,逐一填充pud entry,同時分配並初始化下一階頁表。
(4)如果沒有可能存在的1G block地址映射,這裏的代碼邏輯和上一節中的類似,只不過不斷的循環調用alloc_init_pud改成alloc_init_pmd即可。然而,ARM64的MMU硬件提供了灰常強大的功能,系統支持1G size的block mapping,如果能夠應用會獲取非常多的好處:不需要分配下級的translation table節省了內存,更重要的是大大降低了TLB miss,提高了性能。既然這麼好,當然要使用,不過有什麼條件呢?首先系統配置必須是4k的page size,這種配置下,一個PUD entry可以覆蓋1G的memory block。此外,起止虛擬地址以及映射到的物理地址都必須要對齊在1G size上。
(5)填寫一個PUD描述符,一次搞定1G size的address mapping,沒有PMD和PTE的頁表內存,沒有對PMD 和PTE描述符的訪問,多麼簡單,多麼美妙啊。假設系統內存4G,並且物理地址對齊在1G上(虛擬地址PAGE_OFFSET本來就是對齊在1G的),那麼4個PUD的描述符就搞定了內核空間的線性地址映射區間。
(6)如果pud entry是非空的,那麼就說明之前已經有了對該段地址的mapping(也許是隻映射了部分)。一個簡單的例子就是起始階段的kernel image mapping,在__create_page_tables創建pud 以及pmd中entry。如果不能進行section mapping,那麼還建立了PTE中的描述符,現在這些描述符都沒有用了,我們可以丟棄它們了。
(7)雖然建立了新的頁表,但是舊的頁表還殘留在了TLB中,必須將其“趕盡殺絕”,清除出TLB。
(8)如果pud指向了一個table描述符,也就是說明該entry指向一個PMD table,那麼需要釋放其memory。
六、分配PMD頁表內存並填充相應的描述符
1G block mapping雖好,不一定適合所有的系統,下面我一起看看PUD entry中填充的是block descriptor的情況(描述符指向PMD translation table):
static void alloc_init_pmd(struct mm_struct *mm, pud_t *pud,
unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
void *(*alloc)(unsigned long size))
{
pmd_t *pmd;
unsigned long next;if (pud_none(*pud) || pud_sect(*pud)) {-------------------(1)
pmd = alloc(PTRS_PER_PMD * sizeof(pmd_t));---分配pmd頁表內存
if (pud_sect(*pud)) {--------------------------(2)
split_pud(pud, pmd);
}
pud_populate(mm, pud, pmd);---------------------(3)
flush_tlb_all();
}
BUG_ON(pud_bad(*pud));pmd = pmd_offset(pud, addr);-----------------------(4)
do {
next = pmd_addr_end(addr, end);
if (((addr | next | phys) & ~SECTION_MASK) == 0) {------------(5)
pmd_t old_pmd =*pmd;
set_pmd(pmd, __pmd(phys | pgprot_val(mk_sect_prot(prot))));
if (!pmd_none(old_pmd)) {----------------------(6)
flush_tlb_all();
if (pmd_table(old_pmd)) {
phys_addr_t table = __pa(pte_offset_map(&old_pmd, 0));
if (!WARN_ON_ONCE(slab_is_available()))
memblock_free(table, PAGE_SIZE);
}
}
} else {
alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys),
prot, alloc);
}
phys += next - addr;
} while (pmd++, addr = next, addr != end);
}
(1)有兩個場景需要分配PMD的頁表內存,一個是該pud entry是空的,我們需要分配後續的PMD頁表內存。另外一個是舊的pud entry是section 描述符,映射了1G的address block。但是現在由於種種原因,我們需要修改它,故需要remove這個1G block的section mapping。
(2)雖然是建立新的mapping,但是原來舊的1G mapping也要保留的,也許這次我們只是想更新部分地址映射呢。在這種情況下,我們先通過split_pud函數調用把一個1G block mapping轉換成通過pmd進行mapping的形式(一個pud的section mapping描述符(1G size)變成了512個pmd中的section mapping描述符(2M size)。形式變了,味道不變,加量不加價,仍然是1G block的地址映射。
(3)修正pud entry,讓其指向新的pmd頁表內存,同時flush tlb的內容。
(4)下面這段代碼的邏輯起始是和alloc_init_pud是類似的。如果不能進行2M的section mapping,那麼就循環調用alloc_init_pte進行地址的mapping,這裏我們就不多說了,重點看看2M section mapping的處理。
(5)如果滿足2M section的要求,那麼就調用set_pmd填充pmd entry。
(6)如果有舊的section mapping,並且指向一個PTE table,那麼還需要釋放這些不需要的PTE頁表描述符佔用的內存。
七、分配PTE頁表內存並填充相應的描述符
static void alloc_init_pte(pmd_t *pmd, unsigned long addr,
unsigned long end, unsigned long pfn,
pgprot_t prot,
void *(*alloc)(unsigned long size))
{
pte_t *pte;if (pmd_none(*pmd) || pmd_sect(*pmd)) {----------------(1)
pte = alloc(PTRS_PER_PTE * sizeof(pte_t));
if (pmd_sect(*pmd))
split_pmd(pmd, pte);----------------------(2)
__pmd_populate(pmd, __pa(pte), PMD_TYPE_TABLE);--------(3)
flush_tlb_all();
}
BUG_ON(pmd_bad(*pmd));pte = pte_offset_kernel(pmd, addr);
do {
set_pte(pte, pfn_pte(pfn, prot));-------------------(4)
pfn++;
} while (pte++, addr += PAGE_SIZE, addr != end);
}
(1)走到這個函數,說明後續需要建立PTE這一個level的頁表描述符,因此,需要分配PTE頁表內存,場景有兩個,一個是從來沒有進行過映射,另外一個是已經建立映射,但是是section mapping,不符合要求。
(2)如果之前有section mapping,那麼我們需要將其分裂成512個pte中的page descriptor。
(3)讓pmd entry指向新的pte頁表內存。需要說明的是:如果之前pmd entry是空的,那麼新的pte頁表中有512個invalid descriptor,如果之前有section mapping,那麼實際上這個新的PTE頁表已經通過split_pmd填充了512個page descritor。
(4)循環設定(addr,end)這段地址區域的pte中的page descriptor。